這里深入探討下Javascript的異步編程技術。(P.S. 本文較長,請准備好瓜子可樂 :D)
一. Javascript異步編程簡介
至少在語言級別上,Javascript是單線程的,因此異步編程對其尤為重要。
拿nodejs來說,外殼是一層js語言,這是用戶操作的層面,在這個層次上它是單線程運行的,也就是說我們不能像Java、Python這類語言在語言級別使用多線程能力。取而代之的是,nodejs編程中大量使用了異步編程技術,這是為了高效使用硬件,同時也可以不造成同步阻塞。不過nodejs在底層實現其實還是用了多線程技術,只是這一層用戶對用戶來說是透明的,nodejs幫我們做了幾乎全部的管理工作,我們不用擔心鎖或者其他多線程編程會遇到的問題,只管寫我們的異步代碼就好。
二. Javascript異步編程方法
ES 6以前:
* 回調函數
* 事件監聽(事件發布/訂閱)
* Promise對象
ES 6:
* Generator函數(協程coroutine)
ES 7:
* async和await
PS:如要運行以下例子,請安裝node v0.11以上版本,在命令行下使用 node [文件名.js] 的形式來運行,有部分代碼需要開啟特殊選項,會在具體例子里說明。
1.回調函數
回調函數在Javascript中非常常見,一般是需要在一個耗時操作之后執行某個操作時可以使用回調函數。
example 1:
1 //一個定時器 2 function timer(time, callback){ 3 setTimeout(function(){ 4 callback(); 5 }, time); 6 } 7 8 timer(3000, function(){ 9 console.log(123); 10 })
example 2:
1 //讀文件后輸出文件內容 2 var fs = require('fs'); 3 4 fs.readFile('./text1.txt', 'utf8', function(err, data){ 5 if (err){ 6 throw err; 7 } 8 console.log(data); 9 });
example 3:
1 //嵌套回調,讀一個文件后輸出,再讀另一個文件,注意文件是有序被輸出的,先text1.txt后text2.txt 2 var fs = require('fs'); 3 4 fs.readFile('./text1.txt', 'utf8', function(err, data){ 5 console.log("text1 file content: " + data); 6 fs.readFile('./text2.txt', 'utf8', function(err, data){ 7 console.log("text2 file content: " + data); 8 }); 9 });
example 4:
1 //callback hell 2 3 doSomethingAsync1(function(){ 4 doSomethingAsync2(function(){ 5 doSomethingAsync3(function(){ 6 doSomethingAsync4(function(){ 7 doSomethingAsync5(function(){ 8 // code... 9 }); 10 }); 11 }); 12 }); 13 });
通過觀察以上4個例子,可以發現一個問題,在回調函數嵌套層數不深的情況下,代碼還算容易理解和維護,一旦嵌套層數加深,就會出現“回調金字塔”的問題,就像example 4那樣,如果這里面的每個回調函數中又包含了很多業務邏輯的話,整個代碼塊就會變得非常復雜。從邏輯正確性的角度來說,上面這幾種回調函數的寫法沒有任何問題,但是隨着業務邏輯的增加和趨於復雜,這種寫法的缺點馬上就會暴露出來,想要維護它們實在是太痛苦了,這就是“回調地獄(callback hell)”。
一個衡量回調層次復雜度的方法是,在example 4中,假設doSomethingAsync2要發生在doSomethingAsync1之前,我們需要忍受多少重構的痛苦。
回調函數還有一個問題就是我們在回調函數之外無法捕獲到回調函數中的異常,我們以前在處理異常時一般這么做:
example 5:
1 try{ 2 //do something may cause exception.. 3 } 4 catch(e){ 5 //handle exception... 6 }
在同步代碼中,這沒有問題。現在思考一下下面代碼的執行情況:
example 6:
1 var fs = require('fs'); 2 3 try{ 4 fs.readFile('not_exist_file', 'utf8', function(err, data){ 5 console.log(data); 6 }); 7 } 8 catch(e){ 9 console.log("error caught: " + e); 10 }
你覺得會輸出什么?答案是undefined。我們嘗試讀取一個不存在的文件,這當然會引發異常,但是最外層的try/catch語句卻無法捕獲這個異常。這是異步代碼的執行機制導致的。
Tips: 為什么異步代碼回調函數中的異常無法被最外層的try/catch語句捕獲?
異步調用一般分為兩個階段,提交請求和處理結果,這兩個階段之間有事件循環的調用,它們屬於兩個不同的事件循環(tick),彼此沒有關聯。
異步調用一般以傳入callback的方式來指定異步操作完成后要執行的動作。而異步調用本體和callback屬於不同的事件循環。
try/catch語句只能捕獲當次事件循環的異常,對callback無能為力。
也就是說,一旦我們在異步調用函數中扔出一個異步I/O請求,異步調用函數立即返回,此時,這個異步調用函數和這個異步I/O請求沒有任何關系。
2.事件監聽(事件發布/訂閱)
事件監聽是一種非常常見的異步編程模式,它是一種典型的邏輯分離方式,對代碼解耦很有用處。通常情況下,我們需要考慮哪些部分是不變的,哪些是容易變化的,把不變的部分封裝在組件內部,供外部調用,需要自定義的部分暴露在外部處理。從某種意義上說,事件的設計就是組件的接口設計。
example 7:
1 //發布和訂閱事件 2 3 var events = require('events'); 4 var emitter = new events.EventEmitter(); 5 6 emitter.on('event1', function(message){ 7 console.log(message); 8 }); 9 10 emitter.emit('event1', "message for you");
這種使用事件監聽處理的異步編程方式很適合一些需要高度解耦的場景。例如在之前一個游戲服務端項目中,當人物屬性變化時,需要寫入到持久層。解決方案是先寫一個訂閱方,訂閱'save'事件,在需要保存數據時讓發布方對象(這里就是人物對象)上直接用emit發出一個事件名並攜帶相應參數,訂閱方收到這個事件信息並處理。
3.Promise對象
ES 6中原生提供了Promise對象,Promise對象代表了某個未來才會知道結果的事件(一般是一個異步操作),並且這個事件對外提供了統一的API,可供進一步處理。
使用Promise對象可以用同步操作的流程寫法來表達異步操作,避免了層層嵌套的異步回調,代碼也更加清晰易懂,方便維護。
Promise.prototype.then()
Promise.prototype.then()方法返回的是一個新的Promise對象,因此可以采用鏈式寫法,即一個then后面再調用另一個then。如果前一個回調函數返回的是一個Promise對象,此時后一個回調函數會等待第一個Promise對象有了結果,才會進一步調用。
example 8:
1 //ES 6原生Promise示例 2 var fs = require('fs') 3 4 var read = function (filename){ 5 var promise = new Promise(function(resolve, reject){ 6 fs.readFile(filename, 'utf8', function(err, data){ 7 if (err){ 8 reject(err); 9 } 10 resolve(data); 11 }) 12 }); 13 return promise; 14 } 15 16 read('./text1.txt') 17 .then(function(data){ 18 console.log(data); 19 }, function(err){ 20 console.log("err: " + err); 21 });
以上代碼中,read函數是Promise化的,在read函數中,實例化了一個Promise對象,Promise的構造函數接受一個函數作為參數,這個函數的兩個參數分別是resolve方法和reject方法。如果異步操作成功,就是用resolve方法將Promise對象的狀態從“未完成”變為“完成”(即從pending變為resolved),如果異步操作出錯,則是用reject方法把Promise對象的狀態從“未完成”變為“失敗”(即從pending變為rejected),read函數返回了這個Promise對象。Promise實例生成以后,可以用then方法分別指定resolve方法和reject方法的回調函數。
上面這個例子,Promise構造函數的參數是一個函數,在這個函數中我們寫異步操作的代碼,在異步操作的回調中,我們根據err變量來選擇是執行resolve方法還是reject方法,一般來說調用resolve方法的參數是異步操作獲取到的數據(如果有的話),但還可能是另一個Promise對象,表示異步操作的結果有可能是一個值,也有可能是另一個異步操作,調用reject方法的參數是異步回調用的err參數。
調用read函數時,實際上返回的是一個Promise對象,通過在這個Promise對象上調用then方法並傳入resolve方法和reject方法來指定異步操作成功和失敗后的操作。
example 9:
1 //原生Primose順序嵌套回調示例 2 var fs = require('fs') 3 4 var read = function (filename){ 5 var promise = new Promise(function(resolve, reject){ 6 fs.readFile(filename, 'utf8', function(err, data){ 7 if (err){ 8 reject(err); 9 } 10 resolve(data); 11 }) 12 }); 13 return promise; 14 } 15 16 read('./text1.txt') 17 .then(function(data){ 18 console.log(data); 19 return read('./text2.txt'); 20 }) 21 .then(function(data){ 22 console.log(data); 23 });
在Promise的順序嵌套回調中,第一個then方法先輸出text1.txt的內容后返回read('./text2.txt'),注意這里很關鍵,這里實際上返回了一個新的Promise實例,第二個then方法指定了異步讀取text2.txt文件的回調函數。這種形似同步調用的Promise順序嵌套回調的特點就是有一大堆的then方法,代碼冗余略多。
異常處理
Promise.prototype.catch()
Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。
example 9:
1 var fs = require('fs') 2 3 var read = function (filename){ 4 var promise = new Promise(function(resolve, reject){ 5 fs.readFile(filename, 'utf8', function(err, data){ 6 if (err){ 7 reject(err); 8 } 9 resolve(data); 10 }) 11 }); 12 return promise; 13 } 14 15 read('./text1.txt') 16 .then(function(data){ 17 console.log(data); 18 return read('not_exist_file'); 19 }) 20 .then(function(data){ 21 console.log(data); 22 }) 23 .catch(function(err){ 24 console.log("error caught: " + err); 25 }) 26 .then(function(data){ 27 console.log("completed"); 28 })
使用Promise對象的catch方法可以捕獲異步調用鏈中callback的異常,Promise對象的catch方法返回的也是一個Promise對象,因此,在catch方法后還可以繼續寫異步調用方法。這是一個非常強大的能力。
如果在catch方法中發生了異常:
example 10:
1 var fs = require('fs') 2 3 var read = function (filename){ 4 var promise = new Promise(function(resolve, reject){ 5 fs.readFile(filename, 'utf8', function(err, data){ 6 if (err){ 7 reject(err); 8 } 9 resolve(data); 10 }) 11 }); 12 return promise; 13 } 14 15 read('./text1.txt') 16 .then(function(data){ 17 console.log(data); 18 return read('not_exist_file'); 19 }) 20 .then(function(data){ 21 console.log(data); 22 }) 23 .catch(function(err){ 24 console.log("error caught: " + err); 25 x+1; 26 }) 27 .then(function(data){ 28 console.log("completed"); 29 })
在上述代碼中,x+1會拋出一個異常,但是由於后面沒有catch方法了,導致這個異常不會被捕獲,而且也不會傳遞到外層去,也就是說這個異常就默默發生了,沒有驚動任何人。
我們可以在catch方法后再加catch方法來捕獲這個x+1的異常:
example 11:
1 var fs = require('fs') 2 3 var read = function (filename){ 4 var promise = new Promise(function(resolve, reject){ 5 fs.readFile(filename, 'utf8', function(err, data){ 6 if (err){ 7 reject(err); 8 } 9 resolve(data); 10 }) 11 }); 12 return promise; 13 } 14 15 read('./text1.txt') 16 .then(function(data){ 17 console.log(data); 18 return read('not_exist_file'); 19 }) 20 .then(function(data){ 21 console.log(data); 22 }) 23 .catch(function(err){ 24 console.log("error caught: " + err); 25 x+1; 26 }) 27 .catch(function(err){ 28 console.log("error caught: " + err); 29 }) 30 .then(function(data){ 31 console.log("completed"); 32 })
Promise異步並發
如果幾個異步調用有關聯,但它們不是順序式的,是可以同時進行的,我們很直觀地會希望它們能夠並發執行(這里要注意區分“並發”和“並行”的概念,不要搞混)。
Promise.all()
Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.all([p1,p2,p3]);
Promise.all方法接受一個數組作為參數,p1、p2、p3都是Promise對象實例。
p的狀態由p1、p2、p3決定,分兩種情況:
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
一個具體的例子:
example 12:
1 var fs = require('fs') 2 3 var read = function (filename){ 4 var promise = new Promise(function(resolve, reject){ 5 fs.readFile(filename, 'utf8', function(err, data){ 6 if (err){ 7 reject(err); 8 } 9 resolve(data); 10 }) 11 }); 12 return promise; 13 } 14 15 var promises = [1, 2].map(function(fileno){ 16 return read('./text' + fileno + '.txt'); 17 }); 18 19 Promise.all(promises) 20 .then(function(contents){ 21 console.log(contents); 22 }) 23 .catch(function(err){ 24 console.log("error caught: " + err); 25 })
上述代碼會並發執行讀取text1.txt和text2.txt的操作,當兩個文件都讀取完畢時,輸出它們的內容,contents是一個數組,每個元素對應promises數組的執行結果 (順序完全一致),在這里就是text1.txt和text2.txt的內容。
Promise.race()
Promise.race()也是將多個Promise實例包裝成一個新的Promise實例:
var p = Promise.race([p1,p2,p3]);
上述代碼中,p1、p2、p3只要有一個實例率先改變狀態,p的狀態就會跟着改變,那個率先改變的Promise實例的返回值,就傳遞給p的返回值。如果Promise.all方法和Promise.race方法的參數不是Promise實例,就會先調用下面講到的Promise.resolve方法,將參數轉為Promise實例,再進一步處理。
example 13:
1 var http = require('http'); 2 var qs = require('querystring'); 3 4 var requester = function(options, postData){ 5 var promise = new Promise(function(resolve, reject){ 6 var content = ""; 7 var req = http.request(options, function (res) { 8 res.setEncoding('utf8'); 9 10 res.on('data', function (data) { 11 onData(data); 12 }); 13 14 res.on('end', function () { 15 resolve(content); 16 }); 17 18 function onData(data){ 19 content += data; 20 } 21 }); 22 23 req.on('error', function(err) { 24 reject(err); 25 }); 26 27 req.write(postData); 28 req.end(); 29 }); 30 31 return promise; 32 } 33 34 var promises = ["檸檬", "蘋果"].map(function(keyword){ 35 var options = { 36 hostname: 'localhost', 37 port: 9200, 38 path: '/meiqu/tips/_search', 39 method: 'POST' 40 }; 41 42 var data = { 43 'query' : { 44 'match' : { 45 'summary' : keyword 46 } 47 } 48 }; 49 data = JSON.stringify(data); 50 return requester(options, data); 51 }); 52 53 Promise.race(promises) 54 .then(function(contents) { 55 var obj = JSON.parse(contents); 56 console.log(obj.hits.hits[0]._source.summary); 57 }) 58 .catch(function(err){ 59 console.log(err); 60 });
Promise.resolve()
有時候需將現有對象轉換成Promise對象,可以使用Promise.resolve()。
如果Promise.resolve方法的參數,不是具有then方法的對象(又稱thenable對象),則返回一個新的Promise對象,且它的狀態為fulfilled。
如果Promise.resolve方法的參數是一個Promise對象的實例,則會被原封不動地返回。
example 14:
1 var p = Promise.resolve('Hello'); 2 3 p.then(function (s){ 4 console.log(s) 5 });
Promise.reject()
Promise.reject(reason)方法也會返回一個新的Promise實例,該實例的狀態為rejected。Promise.reject方法的參數reason,會被傳遞給實例的回調函數。
example 15:
1 var p = Promise.reject('出錯了'); 2 3 p.then(null, function (s){ 4 console.log(s) 5 });
上面代碼生成一個Promise對象的實例p,狀態為rejected,回調函數會立即執行。
3.Generator函數
Generator函數是協程在ES 6中的實現,最大特點就是可以交出函數的執行權(暫停執行)。
注意:在node中需要開啟--harmony選項來啟用Generator函數。
整個Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用yield語句注明。
協程的運行方式如下:
第一步:協程A開始運行。
第二步:協程A執行到一半,暫停,執行權轉移到協程B。
第三步:一段時間后,協程B交還執行權。
第四步:協程A恢復執行。
上面的協程A就是異步任務,因為分為兩步執行。
比如一個讀取文件的例子:
example 16:
1 function asnycJob() { 2 // ...其他代碼 3 var f = yield readFile(fileA); 4 // ...其他代碼 5 }
asnycJob函數是一個協程,yield語句表示,執行到此處執行權就交給其他協程,也就是說,yield是兩個階段的分界線。協程遇到yield語句就暫停執行,直到執行權返回,再從暫停處繼續執行。這種寫法的優點是,可以把異步代碼寫得像同步一樣。
看一個簡單的Generator函數例子:
example 17:
1 function* gen(x){ 2 var y = yield x + 2; 3 return y; 4 } 5 6 var g = gen(1); 7 var r1 = g.next(); // { value: 3, done: false } 8 console.log(r1); 9 var r2 = g.next() // { value: undefined, done: true } 10 console.log(r2);
需要注意的是Generator函數的函數名前面有一個"*"。
上述代碼中,調用Generator函數,會返回一個內部指針(即遍歷器)g,這是Generator函數和一般函數不同的地方,調用它不會返回結果,而是一個指針對象。調用指針g的next方法,會移動內部指針,指向第一個遇到的yield語句,上例就是執行到x+2為止。
換言之,next方法的作用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句后面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,即是否還有下一個階段。
Generator函數的數據交換和錯誤處理
next方法返回值的value屬性,是Generator函數向外輸出數據;next方法還可以接受參數,這是向Generator函數體內輸入數據。
example 18:
1 function* gen(x){ 2 var y = yield x + 2; 3 return y; 4 } 5 6 var g = gen(1); 7 var r1 = g.next(); // { value: 3, done: false } 8 console.log(r1); 9 var r2 = g.next(2) // { value: 2, done: true } 10 console.log(r2);
第一個next的value值,返回了表達式x+2的值(3),第二個next帶有參數2,這個參數傳入Generator函數,作為上個階段異步任務的返回結果,被函數體內的變量y接收,因此這一階段的value值就是2。
Generator函數內部還可以部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
example 19:
1 function* gen(x){ 2 try { 3 var y = yield x + 2; 4 } 5 catch (e){ 6 console.log(e); 7 } 8 return y; 9 } 10 11 var g = gen(1); 12 g.next(); 13 g.throw('error!'); //error!
下面是一個讀取文件的真實異步操作的例子。
example 20:
1 var fs = require('fs'); 2 var thunkify = require('thunkify'); 3 var readFile = thunkify(fs.readFile); 4 5 var gen = function* (){ 6 var r1 = yield readFile('./text1.txt', 'utf8'); 7 console.log(r1); 8 var r2 = yield readFile('./text2.txt', 'utf8'); 9 console.log(r2); 10 }; 11 12 //開始執行上面的代碼 13 var g = gen(); 14 15 var r1 = g.next(); 16 r1.value(function(err, data){ 17 if (err) throw err; 18 var r2 = g.next(data); 19 r2.value(function(err, data){ 20 if (err) throw err; 21 g.next(data); 22 }); 23 });
這就是一個基本的Generator函數定義和執行的流程。可以看到,雖然這里的Generator函數寫的很簡潔,和同步方法的寫法很像,但是執行起來卻很麻煩,流程管理比較繁瑣。
在深入討論Generator函數之前我們先要知道Thunk函數這個概念。
求值策略(即函數的參數到底應該何時求值)
(1) 傳值調用
(2) 傳名調用
Javascript是傳值調用的,Thunk函數是編譯器“傳名調用”的實現,就是將參數放入一個臨時函數中,再將這個臨時函數放入函數體,這個臨時函數就叫做Thunk函數。
舉個栗子就好懂了:
example 21:
1 function f(m){ 2 return m * 2; 3 } 4 var x = 1; 5 f(x + 5); 6 7 //等同於 8 var thunk = function (x) { 9 return x + 5; 10 }; 11 12 function f(thunk){ 13 return thunk() * 2; 14 }
Thunk函數本質上是函數柯里化(currying),柯里化進行參數復用和惰性求值,這個是函數式編程的一些技巧,在js中,我們可以利用**高階函數**實現函數柯里化。
JavaScript語言的Thunk函數
在JavaScript語言中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數作為參數。
example 22:
1 var fs = require('fs'); 2 3 // 正常版本的readFile(多參數版本) 4 fs.readFile(fileName, callback); 5 6 // Thunk版本的readFile(單參數版本) 7 var readFileThunk = Thunk(fileName); 8 readFileThunk(callback); 9 10 var Thunk = function (fileName){ 11 return function (callback){ 12 return fs.readFile(fileName, callback); 13 }; 14 };
任何函數,只要參數有回調函數,就能寫成Thunk函數的形式。以下是一個簡單的Thunk函數轉換器:
example 23:
1 var Thunk = function(fn){ 2 return function (){ 3 var args = Array.prototype.slice.call(arguments); 4 return function (callback){ 5 args.push(callback); 6 return fn.apply(this, args); 7 } 8 }; 9 };
從本質上說,我們借助了Javascript高階函數來抽象了異步執行流程。
使用上面的轉換器,生成fs.readFile的Thunk函數。
example 24:
1 var readFileThunk = Thunk(fs.readFile); 2 readFileThunk('./text1.txt', 'utf8')(function(err, data){ 3 console.log(data); 4 });
可以使用thunkify模塊來Thunk化任何帶有callback的函數。
我們需要借助Thunk函數的能力來自動執行Generator函數。
下面是一個基於Thunk函數的Generator函數執行器。
example 25:
1 //Generator函數執行器 2 3 function run(fn) { 4 var gen = fn(); 5 6 function next(err, data) { 7 var result = gen.next(data); 8 if (result.done) return; 9 result.value(next); 10 } 11 12 next(); 13 } 14 15 run(gen);
我們馬上拿這個執行器來做點事情。
example 26:
1 var fs = require('fs'); 2 var thunkify = require('thunkify'); 3 var readFile = thunkify(fs.readFile); 4 5 var gen = function* (){ 6 var f1 = yield readFile('./text1.txt', 'utf8'); 7 console.log(f1); 8 var f2 = yield readFile('./text2.txt', 'utf8'); 9 console.log(f2); 10 var f3 = yield readFile('./text3.txt', 'utf8'); 11 console.log(f3); 12 }; 13 14 function run(fn) { 15 var gen = fn(); 16 17 function next(err, data) { 18 var result = gen.next(data); 19 if (result.done) return; 20 result.value(next); 21 } 22 23 next(); 24 } 25 26 run(gen); //自動執行
現在異步操作代碼的寫法就和同步的寫法一樣了。實際上,Thunk函數並不是自動控制Generator函數執行的唯一方案,要自動控制Generator函數的執行過程,需要有一種機制,自動接收和交還程序的執行權,回調函數和Promise都可以做到(利用調用自身的一些特性)。
yield *語句
普通的yield語句后面跟一個異步操作,yield *語句后面需要跟一個遍歷器,可以理解為yield *后面要跟另一個Generator函數,講起來比較抽象,看一個實例。
example 27:
1 //嵌套異步操作流 2 var fs = require('fs'); 3 var thunkify = require('thunkify'); 4 var readFile = thunkify(fs.readFile); 5 6 var gen = function* (){ 7 var f1 = yield readFile('./text1.txt', 'utf8'); 8 console.log(f1); 9 10 var f_ = yield * gen1(); //此處插入了另外一個異步流程 11 12 var f2 = yield readFile('./text2.txt', 'utf8'); 13 console.log(f2); 14 15 var f3 = yield readFile('./text3.txt', 'utf8'); 16 console.log(f3); 17 }; 18 19 var gen1 = function* (){ 20 var f4 = yield readFile('./text4.txt', 'utf8'); 21 console.log(f4); 22 var f5 = yield readFile('./text5.txt', 'utf8'); 23 console.log(f5); 24 } 25 26 function run(fn) { 27 var gen = fn(); 28 29 function next(err, data) { 30 var result = gen.next(data); 31 if (result.done) return; 32 result.value(next); 33 } 34 35 next(); 36 } 37 38 run(gen); //自動執行
上面這個例子會輸出
1
4
5
2
3
也就是說,使用yield *可以在一個異步操作流程中直接插入另一個異步操作流程,我們可以據此構造可嵌套的異步操作流,更為重要的是,寫這些代碼完全是同步風格的。
目前業界比較流行的Generator函數自動執行的解決方案是co庫,此處也只給出co的例子。順帶一提node-fibers也是一種解決方案。
順序執行3個異步讀取文件的操作,並依次輸出文件內容:
example 28:
1 var fs = require('fs'); 2 var co = require('co'); 3 var thunkify = require('thunkify'); 4 var readFile = thunkify(fs.readFile); 5 6 co(function*(){ 7 var files=[ 8 './text1.txt', 9 './text2.txt', 10 './text3.txt' 11 ]; 12 13 var p1 = yield readFile(files[0]); 14 console.log(files[0] + ' ->' + p1); 15 16 var p2 = yield readFile(files[1]); 17 console.log(files[1] + ' ->' + p2); 18 19 var p3 = yield readFile(files[2]); 20 console.log(files[2] + ' ->' + p3); 21 22 return 'done'; 23 });
並發執行3個異步讀取文件的操作,並存儲在一個數組中輸出(順序和文件名相同):
example 29:
1 var fs = require('fs'); 2 var co = require('co'); 3 var thunkify = require('thunkify'); 4 var readFile = thunkify(fs.readFile); 5 6 co(function* () { 7 var files = ['./text1.txt', './text2.txt', './text3.txt']; 8 var contents = yield files.map(readFileAsync); 9 10 console.log(contents); 11 }); 12 13 function readFileAsync(filename) { 14 return readFile(filename, 'utf8'); 15 }
co庫和我們剛才的run函數有點類似,都是自動控制Generator函數的流程。
ES 7中的async和await
async和await是ES 7中的新語法,新到連ES 6都不支持,但是可以通過Babel一類的預編譯器處理成ES 5的代碼。目前比較一致的看法是async和await是js對異步的終極解決方案。
async函數實際上是Generator函數的語法糖(js最喜歡搞語法糖,包括ES 6中新增的“類”支持其實也是語法糖)。
配置Babel可以看:配置Babel
如果想嘗個鮮,簡單一點做法是執行:
1 sudo npm install --global babel-cli
async_await.js代碼如下:
1 var fs = require('fs'); 2 3 var readFile = function (fileName){ 4 return new Promise(function (resolve, reject){ 5 fs.readFile(fileName, function(error, data){ 6 if (error){ 7 reject(error); 8 } 9 else { 10 resolve(data); 11 } 12 }); 13 }); 14 }; 15 16 var asyncReadFile = async function (){ 17 var f1 = await readFile('./text1.txt'); 18 var f2 = await readFile('./text2.txt'); 19 console.log(f1.toString()); 20 console.log(f2.toString()); 21 }; 22 23 asyncReadFile();
接着執行 babel-node async_await.js
輸出:
1
2