2.深刻解析Javascript異步編程

深刻解析Javascript異步編程

 原文地址:https://www.cnblogs.com/nullcc/p/5841182.html

這裏深刻探討下Javascript的異步編程技術。(P.S. 本文較長,請準備好瓜子可樂 :D)html

一. Javascript異步編程簡介node

至少在語言級別上,Javascript是單線程的,所以異步編程對其尤其重要。git

拿nodejs來講,外殼是一層js語言,這是用戶操做的層面,在這個層次上它是單線程運行的,也就是說咱們不能像Java、Python這類語言在語言級別使用多線程能力。取而代之的是,nodejs編程中大量使用了異步編程技術,這是爲了高效使用硬件,同時也能夠不形成同步阻塞。不過nodejs在底層實現其實仍是用了多線程技術,只是這一層用戶對用戶來講是透明的,nodejs幫咱們作了幾乎所有的管理工做,咱們不用擔憂鎖或者其餘多線程編程會遇到的問題,只管寫咱們的異步代碼就好。github

二. Javascript異步編程方法npm

ES 6之前:編程

* 回調函數
* 事件監聽(事件發佈/訂閱)
* Promise對象數組

ES 6:promise

* Generator函數(協程coroutine)babel

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方法接受一個數組做爲參數,p一、p二、p3都是Promise對象實例。

p的狀態由p一、p二、p3決定,分兩種狀況:

(1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p一、p二、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]);

上述代碼中,p一、p二、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

相關文章
相關標籤/搜索