• async 和 await 的本质


    ​    async 和 await 几乎是 NodeJS 的最典型关键字,最能代表 NodeJS 的特色,然而掌握这两个关键字的原理却不容易。这篇文章使用从零“构建”出 async 和 await 关键字的方式,来帮助理清 async 和 await 的本质。

        先用一句话概括:async 和 await 是内置了执行器的 generator 函数。

        什么是 generator 函数?顾名思义,generator 函数就是一个生成器。生成的是一个可以多次通过 .next() 迭代的对象,例如,定义一个 generator 函数如下:

    let g = function* () {
      yield 1
      yield 2
      return 3
    }

        其中,yield 关键字定义每次迭代的返回值,最后一个返回值用 return。

        然后,就可以用它来生成一个可迭代的对象:

    let iter = g()
    ​
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())

        以上代码执行的结果是:

    { value: 1, done: false }
    { value: 2, done: false }
    { value: 3, done: true }
    { value: undefined, done: true }

        generator 函数也可以接收参数:

    let g = function* (a, b) {
      yield a
      yield b
      return a + b
    }
    ​
    let iter = g(1, 2)
    ​
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())

        执行结果:

    { value: 1, done: false }
    { value: 2, done: false }
    { value: 3, done: true }
    { value: undefined, done: true }

        

        接下来是一个关键点:前面的例子中,调用next() 时并没有传递参数,但是实际上 next() 是可以接受参数的,而且这个参数和 yield 关键字有特殊的关系:

    let g = function* () {
      let ret = yield 1
      return ret
    }
    ​
    let iter = g()
    ​
    console.log(iter.next())
    console.log(iter.next(2))

        以上代码的执行结果是:

    { value: 1, done: false }
    { value: 2, done: true }

        可以看到,next(2) 这个调用导致 ret 的值变成了2。这是为什么呢?因为 next() 的参数会成为 yield 表达式的值。也就是说,

      let ret = yield 1

        这行代码其实是被拆成两段执行的。第一次调用 .next() 的时候,执行到了 yield 1 这里,就暂停并返回了。这时打印 .next() 的返回值是 { value: 1, done: false }。然后,执行 .next(2) 的时候,又回到了 g 里面的代码,从 let ret = 2 开始执行。

        理清楚这一执行过程非常重要。因为,这意味着:

        如果我在 g 里面 yield 一个 Promise 出去,在外面等 Promise 执行完之后,再通过 .next() 的参数把结果传进来,会怎样呢?

    let asyncSum = function(a, b) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(a + b)
        }, 1000)
      })
    }
    ​
    let g = function* () {
      let ret = yield asyncSum(1, 2)
      return ret
    }
    ​
    let iter = g()
    ​
    let p = iter.next().value
    p.then(sum => {
      console.log(iter.next(sum))
    })

        执行结果就是等待一秒之后打印出3:

    // 这里挂起了一秒钟
    { value: 3, done: true }

        请细细品味上面代码里面的 g 函数:

    let g = function* () {
      let ret = yield asyncSum(1, 2)
      return ret
    }

        将其与下面代码进行对比:

    let g = async function () {
      let ret = await asyncSum(1, 2)
      return ret
    }

        是不是特别相似?事实上, async 函数的本质就是 generator 函数,只不过附带了一个执行器。

        这里就引出了执行器的概念。什么叫执行器?让我们回到 g:

    let g = function* () {
      let ret = yield asyncSum(1, 2)
      return ret
    }

        g 作为一个 generator 函数,是有两段的。如果我们想要把 g 从头到尾执行完,需要这样子调用 g:

    let iter = g()
    ​
    let p = iter.next().value // 第一次调用 next(),先执行第一段
    p.then(sum => {
      console.log(iter.next(sum)) // 第二次调用 next(),执行第二段
    })

        这样就特别麻烦,最好有一个函数 executor(),我们可以把 g 当作参数传给它,它会自动把 g 执行完,再把最后的结果返回回来。就像这样:

    executor(g).then(result => {
     console.log(result)
    })

        有没有这样的函数呢?有的,我们把上面那段代码自己封装一下,就可以自己写出一个 executor 来:

    let executor = function(g) {
      return new Promise(resolve => {
        let iter = g()
    ​
        let p = iter.next().value
        p.then(sum => {
          let ret = iter.next(sum)
          resolve(ret.value)
        })
      })
    }
    ​
    executor().then(ret => {
      console.log(ret)
    })

        这个 executor 就叫做 g 的执行器。当然啦,这个执行器只是适用于 g,不够通用,能不能做得更通用一点,使它能执行任何的 generator 函数呢?可以的,如下:

    let asyncSum = function(a, b) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(a + b)
        }, 1000)
      })
    }
    ​
    let asyncMul = function(a, b) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(a * b)
        }, 1000)
      })
    }
    ​
    let g = function* (a, b) {
      let sum = yield asyncSum(1, 2)
      let ret = yield asyncMul(sum, 2)
      return ret
    }
    ​
    function executor(generator, ...args) {
      let iter = generator.apply(this, args)
      let n = iter.next()
      if (n.done) {
        return new Promise(resolve => resolve(n.value))
      } else {
        return new Promise(resolve => {
          n.value.then(ret => {
            _r(iter, ret, resolve)
          });
        });
      }
    }
    ​
    function _r(iter, ret, resolve) {
      let n = iter.next(ret)
      if (n.done) {
        resolve(n.value)
      } else {
        n.value.then(ret => {
          _r(iter, ret, resolve)
        })
      }
    }
    ​
    executor(g, 1, 2).then(ret => {
      console.log(ret)
    })

        执行结果:

    // 这里挂起了两秒钟
    6

        不过即使这样也是个不完善的版本,因为没有考虑错误的情况。其实早在 async 和 await 还没有出现的 2013 年,著名程序员 TJ Holowaychuk 就写了一个完善的 generator 执行器。项目地址:https://github.com/tj/co 。其名字叫 co。典型用法就是:

    co(function* () {
      var result = yield Promise.resolve(true);
      return result;
    }).then(function (value) {
      console.log(value);
    }, function (err) {
      console.error(err.stack);
    });

        所以 async 函数本质上就是内置了执行器的 generator 函数,只不过 NodeJS 引擎帮我们实现了执行器。当我们调用 async 函数时,引擎内部调用了执行器。

        原理到这里就结束了。不过可能有细心的读者发现一个奇怪的现象:为什么 TJ Holowaychuk 的这个模块名字要叫做 co?

        答案是 co 代表 coroutine,也就是协程啦。理解到这里就又更深入一层了,但是这里不展开啦,async 函数是协程在 NodeJS 中的实现形式。

  • 相关阅读:
    UICollectionView 布局
    ios 调用支付宝
    iOS POST 上传图片
    ios 判断字符串是否为空
    让写代码成为每天的习惯
    gocron_跨平台定时任务管理器
    monkey做安卓APP的黑盒自动化测试
    Android自动化测试框架分析
    联合查询中where 和and的区别
    JIRA中导出BUG列表是CSV格式的,打开后是乱码
  • 原文地址:https://www.cnblogs.com/blowing00/p/12469552.html
Copyright © 2020-2023  润新知