对于许多新接触 NodeJS 的人而言,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?