• 上篇:es5、es6、es7中的异步写法


    本作品采用知识共享署名 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语法的特性。
    类似于提供了我们一个状态机的支持。

    但这里有两个问题:

    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对象来激发。

  • 相关阅读:
    Daily Scrum 12.14
    Daily Scrum 12.13
    sss
    sss
    sss
    sss
    sss
    sss
    sss
    sss
  • 原文地址:https://www.cnblogs.com/papertree/p/7152462.html
Copyright © 2020-2023  润新知