.Generator與其餘異步處理方案

1.Generator與其餘異步處理方案javascript

之前,異步編程的方法,大概有下面四種。java

1.1 回調函數node

JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直接調用這個函數。它的英語名字 callback,直譯過來就是」從新調用」。
讀取文件進行處理,是這樣寫的。git

fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});
1
2
3
4
1
2
3
4
上面代碼中,readFile函數的第二個參數,就是回調函數,也就是任務的第二段。等到操做系統返回了 /etc/passwd 這個文件之後,回調函數纔會執行。程序員

一個有趣的問題是,爲何Node.js約定,回調函數的第一個參數,必須是錯誤對象err(若是沒有錯誤,該參數就是 null)?緣由是執行分紅兩段,在這兩段之間拋出的錯誤,程序沒法捕捉,只能看成參數,傳入第二段。github

1.2 事件監聽shell

在DOM監聽中比較常見。編程

1.3 發佈/訂閱json

也就是常說的觀察者模式api

1.4 Promise 對象

回調函數自己並無問題,它的問題出如今多個回調函數嵌套。假定讀取A文件以後,再讀取B文件,代碼以下。

fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
// ...
});
});
1
2
3
4
5
1
2
3
4
5
不難想象,若是依次讀取多個文件,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會亂成一團,沒法管理。這種狀況就稱爲」回調函數噩夢」(callback hell)。
Promise就是爲了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,容許將回調函數的橫向加載,改爲縱向加載。採用Promise,連續讀取多個文件,寫法以下。

var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function(data){
console.log(data.toString());
})
.then(function(){
return readFile(fileB);
})
.then(function(data){
console.log(data.toString());
})
.catch(function(err) {
console.log(err);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Promise 的最大問題是代碼冗餘,原來的任務被Promise 包裝了一下,無論什麼操做,一眼看去都是一堆 then,原來的語義變得很不清楚。

1.5 Generator的方式

ECMAScript 6 (簡稱 ES6 )做爲下一代 javascript 語言,將 JavaScript 異步編程帶入了一個全新的階段。關於異步編程能夠查看下圖:

 

而下面這種連續的執行過程叫作同步的。

 

Generator 函數是協程在 ES6 的實現,最大特色就是能夠交出函數的執行權(即暫停執行)。Generator 函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。除此以外,它還有兩個特性,使它能夠做爲異步編程的完整解決方案:函數體內外的數據交換和錯誤處理機制。
next 方法返回值的 value 屬性,是 Generator 函數向外輸出數據;next 方法還能夠接受參數,這是向 Generator 函數體內輸入數據。以下例:

特性1:暫停執行與恢復執行

function* gen(x){
var y = yield x + 2;
return y;
}
1
2
3
4
1
2
3
4
也就是經過yield來暫停執行,經過next來恢復執行

特性2:函數體內外的數據交換

function* gen(x){
var y = yield x + 2;
return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
經過調用next方法獲取到的value表明函數體向外輸出的數據,而調用next方法傳入的參數自己表明向Generator傳入數據。

特性3:錯誤處理機制

function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}

var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
上面代碼的最後一行,Generator 函數體外,使用指針對象的 throw 方法拋出的錯誤,能夠被函數體內的 try … catch 代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。

下面是Generator處理實際任務的一個例子:

var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
1
2
3
4
5
6
1
2
3
4
5
6
具體的執行過程以下:

var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
1
2
3
4
5
6
7
1
2
3
4
5
6
7
2.thunk函數

2.1 thunk函數基本概念

編譯器的」傳名調用」實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作 Thunk 函數。

function f(m){
return m * 2;
}
f(x + 5);
// 等同於
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
上面代碼中,函數 f 的參數 x + 5 被一個函數替換了。凡是用到原參數的地方,對 Thunk 函數求值便可。這就是 Thunk 函數的定義,它是」傳名調用」的一種實現策略,用來替換某個表達式。

2.2 javascript中的thunk函數

JavaScript 語言是傳值調用,它的 Thunk 函數含義有所不一樣。在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數。

thunk函數的實現機制仍是經過閉包來完成的。其調用分爲三步,首先是傳入一個函數,接着是傳入該函數的全部除了callback之外的參數,最後是傳入回調函數callback

function thunkify(fn){
//第一步:傳入函數
return function(){
//第二步:傳入除了callback之外的參數
var args = new Array(arguments.length);
var ctx = this;
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done){
//第三步:傳入回調函數
var called;
args.push(function(){
if (called) return;
//回調函數只會運行一次
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
3.thunk與Generator強強聯手將程序執行權交還給Generator函數

3.1 Generator的yield返回的必須是thunkify的函數才能遞歸

你可能會問, Thunk 函數有什麼用?回答是之前確實沒什麼用,可是 ES6 有了 Generator 函數,Thunk 函數如今能夠用於 Generator 函數的自動流程管理。

以讀取文件爲例。下面的 Generator 函數封裝了兩個異步操做。

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFile('/etc/fstab');
//1.交出執行權
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
//1.交出執行權
console.log(r2.toString());
};
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
上面代碼中,yield 命令用於將程序的執行權移出 Generator 函數,那麼就須要一種方法,將執行權再交還給 Generator 函數。這種方法就是 Thunk 函數,由於它能夠在回調函數裏,將執行權交還給 Generator 函數。爲了便於理解,咱們先看如何手動執行上面這個 Generator 函數。

var g = gen();
var r1 = g.next();
//2.查看這裏的程序你能夠清楚的看到,這裏是將同一個回調函數反覆的傳入到g.next返回的value中。可是這個返回的value必須是thunkify事後的函數,這樣它只會接受一個參數,那麼就知足這裏的定義了
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
//2.value必須是thunkify的函數纔會只接受一個callback參數
if (err) throw err;
g.next(data);
});
});
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
上面代碼中,變量 g 是 Generator 函數的內部指針,表示目前執行到哪一步。next 方法負責將指針移動到下一步,並返回該步的信息(value 屬性和 done 屬性)。
仔細查看上面的代碼,能夠發現 Generator 函數的執行過程,實際上是將同一個回調函數,反覆傳入 next 方法的 value 屬性。這使得咱們能夠用遞歸來自動完成這個過程。

3.2 使用thunkify來自動執行Generator函數從而將執行權交還給Generator

function run(fn) {
var gen = fn();
//獲取到generator內部指針,這裏的next就是thunk函數的回調函數
function next(err, data) {
var result = gen.next(data);
//獲取generator內部狀態
if (result.done) return;
result.value(next);
//Gnerator的value必須是thunkify函數,此時纔會只接受一個回調函數
}
next();
}
run(gen);
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
下面是一個讀取多個文件的例子:

var gen = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
run(gen);
1
2
3
4
5
6
7
1
2
3
4
5
6
7
上面代碼中,函數 gen 封裝了 n 個異步的讀取文件操做,只要執行 run 函數,這些操做就會自動完成。這樣一來,異步操做不只能夠寫得像同步操做,並且一行代碼就能夠執行。

Thunk 函數並非 Generator 函數自動執行的惟一方案。由於自動執行的關鍵是,必須有一種機制,自動控制 Generator 函數的流程,接收和交還程序的執行權。回調函數能夠作到這一點,Promise 對象也能夠作到這一點。

4.co函數庫實現Generator函數自動執行

co 函數庫是著名程序員 TJ Holowaychuk 於2013年6月發佈的一個小工具,用於 Generator 函數的自動執行。

4.1 co函數庫自動執行Generator,可是yield後必須是promise或者thunk函數

好比,有一個 Generator 函數,用於依次讀取兩個文件。

var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
co 函數庫可讓你不用編寫 Generator 函數的執行器。

var co = require('co');
co(gen);

上面代碼中,Generator 函數只要傳入 co 函數,就會自動執行。co 函數返回一個 Promise 對象,所以能夠用 then 方法添加回調函數。

co(gen).then(function (){
console.log('Generator 函數執行完成');
上面代碼中,等到 Generator 函數執行結束,就會輸出一行提示。相對於thunkify,咱們的co的yeild後能夠是promise或者thunk函數,其中後者是經過遞歸來實現的

4.2 co函數庫自動執行Generator的原理

爲何 co 能夠自動執行 Generator 函數?

前面文章說過,Generator 函數就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。

兩種方法能夠作到這一點。

(1)回調函數。將異步操做包裝成 Thunk 函數,在回調函數裏面交回執行權(見第3部分)。

(2)Promise 對象。將異步操做包裝成 Promise 對象,用 then 方法交回執行權。
co 函數庫其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個庫。使用 co 的前提條件是,Generator 函數的 yield 命令後面,只能是 Thunk 函數或 Promise 對象。

下面展現如何使用Promise來交還執行權:

var fs = require('fs');
var readFile = function (fileName){
//這裏new Promise致使咱們的readFile自己返回的是一個Promise
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
//then方法的回調函數中會獲得這裏的data數據
});
});
};

var gen = function* (){
var f1 = yield readFile('/etc/fstab');
//這裏返回的是一個Promise對象,因此經過g.next().value獲取到的對象能夠繼續調用then方法
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
下面咱們手動執行上面這個Generator函數

var g = gen();
//g.next()開始執行第一個readFile方法,g.next().value表示執行第一個readFile返回的promise對象
g.next().value.then(function(data){
//g.next(data)表示將上一個yield的執行結果交還給Generator,至關於將結果賦值給變量f1
g.next(data).value.then(function(data){
g.next(data);
});
注意:若是是下面這樣,那麼f1最後將會是undefined(toString報錯),由於第一個yield執行結果並無交還給Generator,因此沒法獲取到內容:

var g = gen();
g.next().value.then(function(data){
//下面不是g.next(data),因此第一個讀取文件的結果沒有交還給Generator的f1
g.next().value.then(function(data){
g.next(data);

而下面展現的就是一個經過Promise來自動執行Generator的實例:

function run(gen){
var g = gen();
//獲取指針
function next(data){
var result = g.next(data);
if (result.done) return result.value;
//result.value此處返回的是Promise對象
result.value.then(function(data){
next(data);
//將data交給上一個yield執行結果
});
}
next();
}
run(gen);
5.async對於異步的終極解決方案

5.1 Generator函數的async表達

一句話,async 函數就是 Generator 函數的語法糖。

前文有一個 Generator 函數,依次讀取兩個文件。

var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};

var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
寫成 async 函數,就是下面這樣。

var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString( www.ysylcsvip.cn));

一比較就會發現,async 函數就是將 Generator 函數的星號(*)替換成 async,將 yield 替換成 await,僅此而已。

5.2 async函數的優勢

async 函數對 Generator 函數的改進,體如今如下三點。

(1)內置執行器。 Generator 函數的執行必須靠執行器,因此纔有了 co 函數庫,而 async 函數自帶執行器。也就是說,async 函數的執行,與普通函數如出一轍,只要一行。

var result = asyncReadFile();
1
1
(2)更好的語義。 async 和 await,比起星號和 yield,語義更清楚了。async 表示函數裏有異步操做,await 表示緊跟在後面的表達式須要等待結果。

(3)更廣的適用性。 co 函數庫約定,yield 命令後面只能是 Thunk 函數或 Promise 對象,而 async 函數的 await 命令後面,能夠跟 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。

5.3 async的用法

同 Generator 函數同樣,async 函數返回一個 Promise 對象,可使用 then 方法添加回調函數。當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的異步操做完成,再接着執行函數體內後面的語句。

下面的例子,指定多少毫秒後輸出一個值。

function timeout(ms) {
return new Promise((www.boayulevip.cn resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
//遇到await了,全部先返回,獲得異步操做完成,執行後面的代碼
console.log(value)
}
asyncPrint('hello world', 50);
上面代碼指定50毫秒之後,輸出」www.yszxylpt.com hello world」。

5.4 async自動執行器的實現

//genF是Generator函數function spawn(genF) { //返回promise和co同樣,可是co只能是promise和thunk函數 return new Promise(function(resolve, reject) { var gen = genF(); //獲得Generator內部指針 function step(nextF) { try { var next = nextF(); //next獲取到第一個await返回的結果 } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } //若是done爲true那麼咱們直接resolve Promise.resolve(next.value).then(function(v) { //第一個await返回的對象的value表示結果{value:'',done:false} step(function() { return gen.next(v); }); //調用gen.next()獲取到下一個await的結果並傳入上一次的await調用後獲得的value }, function(e) { step(function() { www.vboyule66.cn return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); //首次執行的時候傳入第一個await的data爲undefined });

相關文章
相關標籤/搜索