上篇:es五、es六、es7中的異步寫法

本做品採用知識共享署名 4.0 國際許可協議進行許可。轉載聯繫做者並保留聲明頭部與原文連接https://luzeshu.com/blog/es-async
本博客同步在http://www.cnblogs.com/papertree/p/7152462.htmljavascript


1.1 es5 —— 回調

把異步執行的函數放進回調函數中是最原始的作法。
可是異步的層次太多時,出現的回調嵌套致使代碼至關難看而且難以維護。html

taskAsyncA(function () {
  taskAsyncB(function () {
    taskAsyncC(function () {
      ...
    })
  });
});

因而出現了不少異步流程控制的包。說白了就是把多層嵌套的異步代碼展平了。
如async.js 和 bluebird/Promise。java

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這些包。node

1.2.1 例子1

下面展現了定義一個Generator函數的語法。
調用Generator函數時返回一個Iterator迭代器。經過該迭代器可以不斷觸發Generator函數裏面的yield步驟。
iter.next()返回的是一個含有value、done屬性的對象,done表示是否到達結尾,value表示yield的返回值。
這裏須要注意的是,Generator函數調用時返回一個Iterator,可是自己的代碼是中止的,等iter.next()纔會開始執行。es6

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());

輸出:express

[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是一個{}。promise

一樣的,把第14行的「yield gen2()」修改爲「yield gen2」。那麼也只是把gen2函數當成一個普通的對象返回了。對應的輸出是:app

{ value: [GeneratorFunction: gen2], done: false }

那麼咱們在用koa@1的時候,常常有「yield next」(等效於「yield* next」),這個next實際上就是一個 對象。它所達到的效果,是經過 實現的。下篇博客再講。koa

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語法的特性。
相似於提供了咱們一個狀態機的支持。

但這裏有兩個問題:

  1. 在例子4中yield 一個Promise對象,並不會有什麼特殊現象。不會等待Promise對象被settle以後才繼續往下。
  2. generator函數返回的只是一個Iterator對象,咱們不得不手動調用next()方法去進入下一個狀態。

當用co庫時:

  1. co(function*() {}),這裏面的Generator是會自動依次next下去,直到結束。
  2. yield 一個Promise對象時,等到被settle以後纔會繼續。也正是由於co的這個實現,得以讓咱們寫出「同步形式」而「異步本質」的代碼。
  3. 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對象來激發。

相關文章
相關標籤/搜索