本作品采用知識共享署名 4.0 國際許可協議進行許可。轉載聯系作者並保留聲明頭部與原文鏈接https://luzeshu.com/blog/es-async
本博客同步在http://www.cnblogs.com/papertree/p/7152462.html
1.1 es5 —— 回調
把異步執行的函數放進回調函數中是最原始的做法。
但是異步的層次太多時,出現的回調嵌套導致代碼相當難看並且難以維護。
taskAsyncA(function () {
taskAsyncB(function () {
taskAsyncC(function () {
...
})
});
});
於是出現了很多異步流程控制的包。說白了就是把多層嵌套的異步代碼展平了。
如async.js 和 bluebird/Promise。
1.1.1 async
async.series(function (cb) {
taskAsyncA(function () {
...
return cb();
});
}, function(cb) {
taskAsyncB(function () {
return cb();
});
}, function(cb) {
taskAsyncC(function () {
return cb();
});
....
}, function (err) {
});
1.1.2 bluebird/Promise
taskPromisifyA = Promise.promisify(taskAsyncA);
taskPromisifyB = Promise.promisify(taskAsyncB);
taskPromisifyC = Promise.promisify(taskAsyncC);
.....
Promise.resolve()
.then(() => taskPromisifyA())
.then(() => taskPromisifyB())
.then(() => taskPromisifyC())
......
1.2 es6/es2015 —— generator函數和yield
es6標准多了一些新語法Generator函數、Iterator對象、Promise對象、yield語句。
es6的Promise對象是原生的,不依賴bluebird這些包。
1.2.1 例子1
下面展示了定義一個Generator函數的語法。
調用Generator函數時返回一個Iterator迭代器。通過該迭代器能夠不斷觸發Generator函數里面的yield步驟。
iter.next()返回的是一個含有value、done屬性的對象,done表示是否到達結尾,value表示yield的返回值。
這里需要注意的是,Generator函數調用時返回一個Iterator,但是本身的代碼是停止的,等iter.next()才會開始執行。
2 function* gen() {
3 console.log('step 1');
4 yield 'str 1';
5 console.log('step 2');
6 yield;
7 yield;
8 return 'str 2';
9 }
10
11 let iter = gen();
12 console.log(iter.contructor);
13 console.log('start!');
14 console.log(iter.next());
15 console.log(iter.next());
16 console.log(iter.next());
17 console.log(iter.next());
18 console.log(iter.next());
輸出:
[Sherlock@Holmes Moriarty]$ node app.js
undefined
start!
step 1
{ value: 'str 1', done: false }
step 2
{ value: undefined, done: false }
{ value: undefined, done: false }
{ value: 'str 2', done: true }
{ value: undefined, done: true }
1.2.2 例子2
如果在Generator函數里面,再yield一個generator函數或者Iterator對象,實際上不會串聯到一起。看一下下面的例子就明白了。
1 function* gen2() {
2 console.log('gen2: step1');
3 yield 'str3 in gen2';
4 console.log('gen2: ste2');
5 yield;
6 yield;
7 return 'str4 in gen2';
8 }
9
10 function* gen() {
11 console.log('step 1');
12 yield 'str 1';
13 console.log('step 2');
14 yield gen2();
15 yield;
16 return 'str 2';
17 }
18
19 let iter = gen();
20 console.log(iter.contructor);
21 console.log('start!');
22 console.log(iter.next());
23 console.log(iter.next());
24 console.log(iter.next());
25 console.log(iter.next());
26 console.log(iter.next());
與例子1的輸出基本一樣。第14行代碼所執行的,僅僅是gen2()返回了一個普通的Iterator對象,再被yield當成普通的返回值返回了而已。所以該行輸出的value是一個{}。
同樣的,把第14行的“yield gen2()”修改成“yield gen2”。那么也只是把gen2函數當成一個普通的對象返回了。對應的輸出是:
{ value: [GeneratorFunction: gen2], done: false }
那么我們在用koa@1的時候,經常有“yield next”(等效於“yield* next”),這個next實際上就是一個 對象。它所達到的效果,是通過 實現的。下篇博客再講。
1.2.3 例子3 yield*
yield* 后面跟着一個可迭代對象(iterable object)。包括Iterator對象、數組、字符串、arguments對象等等。
如果希望兩個Generator函數串聯到一起,應該把例子2中的第14行代碼“yield gen2()”改成“yield* gen2()”。此時的輸出為:
[Sherlock@Holmes Moriarty]$ node app.js
undefined
start!
step 1
{ value: 'str 1', done: false }
step 2
gen2: step1
{ value: 'str3 in gen2', done: false }
gen2: ste2
{ value: undefined, done: false }
{ value: undefined, done: false }
{ value: undefined, done: false }
{ value: 'str 2', done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }
但gen2()return的'str4 in gen2'沒有被輸出。當把14行代碼再次改成“console.log(yield* gen2())”時,才會把return回來的結果輸出,而且也不同於yield 返回的對象類型。
輸出結果:
[Sherlock@Holmes Moriarty]$ node app.js
undefined
start!
step 1
{ value: 'str 1', done: false }
step 2
gen2: step1
{ value: 'str3 in gen2', done: false }
gen2: ste2
{ value: undefined, done: false }
{ value: undefined, done: false }
str4 in gen2
{ value: undefined, done: false }
{ value: 'str 2', done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }
關於yield*語句的說明:
The yield* expression iterates over the operand and yields each value returned by it.
The value of yield* expression itself is the value returned by that iterator when it's closed (i.e., when done is true).
(來自https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*)
1.2.4 例子4
如果在Generator函數里面yield一個Promise對象。同樣不會有任何特殊的地方,Promise對象會被yield返回,並且輸出"value: Promise {
例子代碼:
1 function pro() {
2 return new Promise((resolve) => {
3 console.log('waiting...');
4 setTimeout(() => {
5 console.log('timeout');
6 return resolve();
7 }, 3000);
8 });
9 }
10
11 function* gen() {
12 console.log('step 1');
13 yield 'str 1';
14 console.log('step 2');
15 yield pro();
16 yield;
17 return 'str 2';
18 }
19
20 let iter = gen();
21 console.log(iter.contructor);
22 console.log('start!');
23 console.log(iter.next());
24 console.log(iter.next());
25 console.log(iter.next());
26 console.log(iter.next());
27 console.log(iter.next());
輸出:
[Sherlock@Holmes Moriarty]$ node app.js
undefined
start!
step 1
{ value: 'str 1', done: false }
step 2
waiting...
{ value: Promise { <pending> }, done: false }
{ value: undefined, done: false }
{ value: 'str 2', done: true }
{ value: undefined, done: true }
timeout
執行時在{value: undefined, done: true}和timeout之間等待了3秒。
1.2.5 co庫
上面四個例子大概展示了es6的Generator和Iterator語法的特性。
類似於提供了我們一個狀態機的支持。
但這里有兩個問題:
- 在例子4中yield 一個Promise對象,並不會有什么特殊現象。不會等待Promise對象被settle之后才繼續往下。
- generator函數返回的只是一個Iterator對象,我們不得不手動調用next()方法去進入下一個狀態。
當用co庫時:
- co(function*() {}),這里面的Generator是會自動依次next下去,直到結束。
- yield 一個Promise對象時,等到被settle之后才會繼續。也正是因為co的這個實現,得以讓我們寫出“同步形式”而“異步本質”的代碼。
- co激發的Generator函數里面,對yield返回的東西有特殊要求,比如不能是String、undefined這些。而這些在正常es6語法下是允許的。
例子5:
2 const co = require('co'); // 4.6.0版本
3 function pro() {
4 return new Promise((resolve) => {
5 console.log('waiting...');
6 setTimeout(() => {
7 console.log('timeout');
8 return resolve();
9 }, 3000);
10 });
11 }
12
13 function* gen() {
14 console.log('step 1');
15 // yield 'str 1';
16 console.log('step 2');
17 yield pro();
18 console.log('step 3');
19 // yield;
20 return 'str 2';
21 }
22
23 co(gen);
輸出:
[Sherlock@Holmes Moriarty]$ node app.js
step 1
step 2
waiting...
timeout
step 3
可以看出'step 3'的輸出等到promise被settle之后才執行。
例子6:
如果取消第15行代碼注釋,yield 一個字符串或者undefined等,則報錯:
[Sherlock@Holmes Moriarty]$ node app.js
step 1
(node:29050) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: You may only yield a function, promise, generator, array, or object, but the following object was passed: "str 1"
(node:29050) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
co 中的yield Iterator對象
在1.2.2的例子2中做過一個試驗,第14行代碼yield了gen2()返回的Iterator對象之后,gen2()並不會被執行,並且yield gen2()輸出的值僅僅只是“value: {}, done: false”這樣的普通對象。
而如果通過yield* gen2(),在1.2.3中的例子可以看到是會執行gen2()的。
但是在koa1中的中間件里面,“yield* next”和“yield next”是一樣的效果,都能夠讓中間件鏈繼續往下執行。
這里面的原因正是koa1依賴的co庫做了處理。
在co里面,yield一個Iterator對象和yield* 一個Iterator對象,效果是一樣的。
例子7:
1 const co = require('co');
2
3 function* gen2() {
4 console.log('gen2: step1');
5 return 'str4 in gen2';
6 }
7
8 function* gen() {
9 console.log('step 1');
10 yield *gen2();
11 console.log('step 2');
12 return 'str 2';
13 }
14
15 co(gen);
co源碼
上面那個異常怎么拋出的呢?可以來跟蹤一下co源碼流程。co源碼相當小。
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
co()的參數可以是Generator函數也可以是返回Promise對象的函數。
如果是Generator函數,返回了Iterator對象,進入到onFulfilled(),並進入“永動機”的環節。
每一次yield回來的東西調用next,如果是不允許的類型(比如string、undefined等),就會產生一個TypeError並進入onRejected()。
如果是Proise對象,就等待settle。如果是Generator函數,就繼續用co包裝……
如果我們yield 回去的promise對象、或者co自己產生的TypeError,最終都去到onRejected(err)。
1.3 es7 —— async函數與await語句
1.2 說了Generator本質上有點類似狀態機,yield 一個promise對象本身不會等待該promise被settle,也自然無法等待一個異步回調。而co庫利用Generator特性去實現了。
在es7的新特性中,引入了async函數和await語句。await語句生來就是用來等待一個Promise對象的。而且await語法返回值是該Promise對象的resolve值。見下面例子:
The await operator is used to waiting for a Promise. It can only be used inside an async function.
(來自https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
例子:
2 function async1() {
3 return new Promise((resolve) => {
4 console.log('waiting...');
5 setTimeout(() => {
6 console.log('timeout');
7 return resolve('resolve value');
8 }, 3000);
9 });
10 }
11
12 (async function () {
13 let ret = await async1();
14 console.log(ret);
15 })();
輸出:
[Sherlock@Holmes Moriarty]$ node app.js
waiting...
timeout
resolve value
此外,async函數被執行時同普通函數一樣,自動往下執行。而不像Generator函數需要一個Iterator對象來激發。