在上一篇中咱們梳理了koa當中中間件的洋蔥模型執行原理,並實現了一個可讓洋蔥模型自動跑起來的流程管理函數。這一篇,咱們再來研究一下koa當中異步回調同步化寫法的原理,一樣的,咱們也會實現一個管理函數,是的咱們可以經過同步化的寫法來寫異步回調函數。javascript
咱們都知道javascript是一門單線程異步非阻塞語言。異步非阻塞固然是它的一個優勢,但大量的異步操做必然涉及大量的回調函數,特別是當異步嵌套的時候,就會出現回調金字塔的問題,使得代碼的可讀性很是差。好比下面一個例子:java
var fs = require('fs'); fs.readFile('./file1', function(err, data) { console.log(data.toString()); fs.readFile('./file2', function(err, data) { console.log(data.toString()); }) })
這個例子是前後讀取兩個文件內容並打印,其中file2的讀取必須在file1讀取結束以後再進行,所以其操做必需要在file1讀取的回調函數中執行。這是一個典型的回調嵌套,而且只有兩層而已,在實際編程中,咱們可能會遇到更多層的嵌套,這樣的代碼寫法無疑是不夠優雅的。python
在咱們想象中,比較優雅的一種寫法應該是看似同步實則異步的寫法,相似下面這樣:git
var data; data = readFile('./file1'); //下面的代碼是第一個readFile執行完畢以後的回調部分 console.log(data.toString()); //下面的代碼是第二個readFile的回調 data = readFile('./file2'); console.log(data.toString());
這樣的寫法,就徹底避免回調地獄。事實上,koa就讓咱們可使用這樣的寫法來寫異步回調函數:es6
var koa = require('koa'); var app = koa(); var request=require('some module'); app.use(function*() { var data = yield request('http://www.baidu.com'); //如下是異步回調部分 this.body = data.toString(); }) app.listen(3000);
那麼,到底是什麼讓koa有這麼神奇的魔力呢?github
關鍵的一點,其實前一篇也提到了,就是generator具備相似"打斷點"這樣的效果。當遇到yield的時候,就會暫停,將控制權交給yield後面的函數,當下次返回的時候,再繼續執行。編程
而在上面的那個koa例子中,yield後面的可不是任何對象均可以哦!必須是特定類型。在co函數中,能夠支持promise, thunk函數等。promise
今天的文章中,咱們就以promise爲例來進行分析,看看如何使用generator和promise配合,實現異步同步化。app
依舊以第一個讀取文件例子來分析。首先,咱們須要將讀文件的函數進行改造,將其封裝成爲一個promise對象:koa
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //下面是readFile使用的示例 var tmp = readFile('./file1'); tmp.then(function(data) { console.log(data.toString()); })
關於promise的使用,若是不熟悉的能夠去看看es6中的語法。(近期我也會寫一篇文章來教你們如何用es5的語法來本身實現一個具有基本功能的promise對象,敬請期待呦^_^)
簡單來說,promise能夠實現將回調函數經過 promise.then(callback)的形式來寫。可是咱們的目標是配合generator,真正實現如絲般順滑的同步化寫法,如何配合呢,看這段代碼:
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //將讀文件的過程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //手動執行generator var g = gen(); var another = g.next(); //another.value就是返回的promise對象 another.value.then(function(data) { //再次調用g.next從斷點處執行generator,並將data做爲參數傳回 var another2 = g.next(data); another2.value.then(function(data) { g.next(data); }) })
上述代碼中,咱們在generator中yield了readFile,回調語句代碼寫在yield以後的代碼中,徹底是同步的寫法,實現了文章一開頭的設想。
而yield以後,咱們獲得的是一個another.value是一個promise對象,咱們可使用then語句定義回調函數,函數的內容呢,則是將讀取到的data返回給generator並繼續讓generator從斷點處執行。
基本上這就是異步回調同步化最核心的原理,事實上若是你們熟悉python,會知道python中有"協程"的概念,基本上也是使用generator來實現的(我想當懷疑es6的generator就是借鑑了python~)
不過呢,上述代碼咱們依然是手動執行的。那麼同上一篇同樣,咱們還須要實現一個run函數,用於管理generator的流程,讓它可以自動跑起來!
仔細觀察上一段代碼中手動執行generator的部分,也能發現一個規律,這個規律讓咱們能夠直接寫一個遞歸的函數來代替:
var run=function(gen){ var g; if(typeof gen.next==='function'){ g=gen; }else{ g=gen(); } function next(data){ var tmp=g.next(data); if(tmp.done){ return ; }else{ tmp.value.then(next); } } next(); }
函數接收一個generator,並讓其中的異步可以自動執行。使用這個run函數,咱們來讓上一個異步代碼自動執行:
var fs = require('fs'); var run = function(gen) { var g; if (typeof gen.next === 'function') { g = gen; } else { g = gen(); } function next(data) { var tmp = g.next(data); if (tmp.done) { return; } else { tmp.value.then(next); } } next(); } var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //將讀文件的過程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //下面只須要將gen放入run當中便可自動執行 run(gen);
執行上述代碼,便可看到終端依次打印出了file1和file2的內容。
須要指出的是,這裏的run函數爲了簡單起見只支持promise,而實際的co函數還支持thunk等。
這樣一來,co函數的兩大功能基本就完整介紹了,一個是洋蔥模型的流程控制,另外一個是異步同步化代碼的自動執行。在下一篇文章中,我將帶你們對這兩個功能進行整合,寫出咱們本身的一個co函數!
這篇文章的代碼一樣能夠在github上面找到:https://github.com/mly-zju/async-js-demo,其中promise_generator.js就是本篇的示例源碼。
一樣歡迎你們多多關注個人github pages我的博客哦,會不按期更新個人技術文章~