[NodeJS] async 和 await 的本质

栏目: IT技术 · 发布时间: 4年前

内容简介:​    绝大多数nodejs程序员都会使用 async 和 await 关键字,但是极少有人能真正弄明白 async 和 await 的原理。这篇文章将从零“构建”出 async 和 await 关键字,从而帮助理清 async 和 await 的本质。先用一句话概括:async 和 await 是内置了执行器的 generator 函数。什么是 generator 函数?顾名思义,generator 函数就是一个生成器。生成的是一个可以多次通过 .next() 迭代的对象,例如,定义一个 generat

​    绝大多数nodejs程序员都会使用 async 和 await 关键字,但是极少有人能真正弄明白 async 和 await 的原理。这篇文章将从零“构建”出 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 }

接下来是一个关键点:yield 关键字的值和 .next() 的参数的关系:

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,这个 2 被赋值给了 ret。也就是说,

  let ret = yield 1

这行代码其实是被拆成两段执行的。第一次调用 .next() 的时候,g 里面的代码开始执行,执行到了 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)
  console.log(ret)
  return ret
}
​
let iter = g()
​
let p = iter.next().value
p.then(sum => {
  iter.next(sum)
})

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

// 这里挂起了一秒钟
3

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

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

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

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

发现了吧?事实上 async 函数就是 generator 函数。

读者会问了,不对啊,我们调用 async 函数,都是直接调用,返回一个 Promise ,而不用像上面调用 g 那么麻烦的。

没错。上面调用 g 的代码:

let iter = g()
​
let p = iter.next().value
p.then(sum => {
  iter.next(sum)
})

叫做 g 的执行器。我们可以把它封装起来:

let executor = function() {
  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)
})

执行结果:

// 挂起一秒钟
3 // g 里面的 console.log(ret)
3 // .then 里面的 console.log(ret)

实际上,node的执行引擎悄悄地帮我们做了上面的事情,当我们直接调用一个 async 函数时,其实是在调用它的执行器。

原理讲到这里就完了。下面是扩展部分。

上面的 executor 函数是仅仅针对这个例子里面的 g 写的。那我们是否可能写一个通用的执行器函数,适用于任何 generator 函数呢?不管 generator 函数里面有多少个 yield ,这个执行器是否都可以自动全部处理完?

答案当然是肯定的,用到了递归,请看完整代码:

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

不过上面这个 executor 是个不完善的版本,因为没有考虑错误的情况。其实早在 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 和 await 的本质,到这里就结束啦。文章最后请细心的读者思考一个问题:为什么 TJ Holowaychuk 的这个模块名字要叫做 co?


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机和难解性

计算机和难解性

M.R 加里、D.S. 约翰逊 / 张立昂、沈泓 / 科学出版社 / 1987年 / 4.50

本书系统地介绍了NP完全性理论的概念和方法,全书共分为7章和两个附录。第一章粗略地介绍了计算复杂性的一些基本概念和NP完全性理论的意义。第二章至第五章介绍了NP完全性的基本理论和证明的方法。第六章集中研究NP难问题的近似算法。第七章概述了大量计算复杂性中的有关理论课题。 附录A收集了范围广泛、内容丰富的NP完全性和NP难的问题、附录B补充了NP问题的一些最新的进展,既有理论方面的,又有关于具体问题......一起来看看 《计算机和难解性》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具