Shio Y. Blog

async/await 是如何实现的?

ES2017 引入了Async/Await语法,使得我们的异步代码看起来更像是同步代码,隐藏了成堆的回调函数。但是 Javascript 引擎内部是如何实现的呢?

先来看看 Babel 的实现

在前几年浏览器还没有完全原生支持async语法时,为了抢先体验 ECMA Spec 中的新功能,我们一般使用 Babel 将最新语法转译Transpile成浏览器支持的语法结构

如:


_10
async function foo(url) {
_10
try {
_10
const response = await fetch(url);
_10
console.log(await response.text());
_10
}
_10
catch (err) {
_10
console.log('fetch failed', err);
_10
}
_10
}

转译成了


_19
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
_19
_19
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
_19
_19
function foo(_x) {
_19
return _foo.apply(this, arguments);
_19
}
_19
_19
function _foo() {
_19
_foo = _asyncToGenerator(function* g_foo(url) {
_19
try {
_19
const response = yield fetch(url);
_19
console.log(yield response.text());
_19
} catch (err) {
_19
console.log('fetch failed', err);
_19
}
_19
});
_19
return _foo.apply(this, arguments);
_19
}

Babel 替我们生成了两个 helper 函数,如果我们对它稍加简化,可以得到:


_22
function _asyncToGenerator(fn) {
_22
return function () {
_22
var gen = fn.apply(this, arguments);
_22
return new Promise(function (resolve) {
_22
function step(key, arg) {
_22
var info = gen[key](arg);
_22
var value = info.value;
_22
if (info.done) {
_22
resolve(value);
_22
} else {
_22
// 如果value不是一个promise,则将它转化成一个resolved promise
_22
Promise.resolve(value).then(function(val) {
_22
step("next", val)
_22
}, function(err) {
_22
step("throw", err)
_22
});
_22
}
_22
}
_22
step("next")
_22
});
_22
};
_22
}

生成器 Generator

在这里例子中异步函数被转换成了生成器函数。这里先简要回顾一下生成器:

  • 通过function* () {}定义生成器函数,其返回一个生成器对象
  • 调用生成器函数的.next()方法后,开始执行生成器函数代码
  • 生成器对象中可使用yield关键字,生成器函数执行时遇到yield将暂停函数的执行,转而执行.next()之后的代码
  • 再次调用.next()方法时,从之前yield的位置继续执行

生成器函数与主函数之间可进行数据传递:

  • 主函数 -> 生成器函数:gen.next(val) 参见🔗
  • 生成器函数 -> 主函数:yield val

_asyncToGenerator 在干什么?

我们知道在async函数await一个 promise 对象,我们会等到它 fulfilled 以后开始执行后面的代码。在转换后的生成器函数中,yield相当于await。然后我们调用gen.next()方法执行生成器函数,取得yield fetch(url)中的 promise,并.then()方法中进行递归——调用gen.next(arg)继续执行生成器函数,promise resolve 的值通过arg传递给生成器函数, ...

具体步骤为:

  1. 创建生成器对象gen,async 函数返回一个 promise

  2. 执行传入 promise 的函数,调用gen.next(),引擎跳转到生成器函数g_foo开始执行

  3. 遇到yield,暂停函数执行,fetch(url)返回一个 promise,并被传回给主函数,赋值给info.value

  4. 调用promise.then设置 resolved 后的回调函数,_asyncToGenerator结束运行

  5. 此时将继续执行主函数中的剩余代码。

  6. 当 fetch 从服务器得到数据后,执行回调函数,即step,并将promise resolved后的值传入其中

  7. 重复执行2~6,知道生成器函数返回,done为真

浏览器(V8)实际是如何处理的?

根据 V8 官方的 blog,在 Node.js 12 的一系列优化后,async函数在内部被转化成了


_17
async function foo(v) {
_17
const w = await v
_17
return w
_17
}
_17
_17
resumable function foo(v) {
_17
implicit_promise = createPromise()
_17
// 如果v不是promise,则将其转化成promise
_17
promise = promiseResolve(v)
_17
// 设置fulfilled和rejected时的回调函数,恢复foo的运行
_17
performPromiseThen(promise,
_17
res => resume(<<foo>>, res),
_17
err => throw(<<foo>>, err))
_17
// 挂起foo,并返回隐式创建的promise
_17
w = suspend(<<foo>>, implicit_promise)
_17
resolvePromise(implicit_promise, w)
_17
}

观察发现代码结构其实与 Babel polyfill 十分相似,内部同样使用Promise,resumesuspend这些引擎内部函数与 generator 也有异曲同工之妙。performPromiseThen在引擎的微任务队列中创建了PromiseReactionJob,与回调函数绑定。当主函数运行完,且await的promise resolve了之后,执行微任务,恢复foo函数的执行。

写于 2022年07月17日