ES2017 引入了Async/Await
语法,使得我们的异步代码看起来更像是同步代码,隐藏了成堆的回调函数。但是 Javascript 引擎内部是如何实现的呢?
先来看看 Babel 的实现
在前几年浏览器还没有完全原生支持async
语法时,为了抢先体验 ECMA Spec 中的新功能,我们一般使用 Babel 将最新语法转译Transpile
成浏览器支持的语法结构
如:
_10async 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}
转译成了
_19function 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_19function _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_19function foo(_x) {_19 return _foo.apply(this, arguments);_19}_19_19function _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 函数,如果我们对它稍加简化,可以得到:
_22function _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
传递给生成器函数, ...
具体步骤为:
-
创建生成器对象
gen
,async 函数返回一个 promise -
执行传入 promise 的函数,调用
gen.next()
,引擎跳转到生成器函数g_foo
开始执行 -
遇到
yield
,暂停函数执行,fetch(url)
返回一个 promise,并被传回给主函数,赋值给info.value
-
调用
promise.then
设置 resolved 后的回调函数,_asyncToGenerator
结束运行 -
此时将继续执行主函数中的剩余代码。
-
当 fetch 从服务器得到数据后,执行回调函数,即
step
,并将promise resolved后的值传入其中 -
重复执行2~6,知道生成器函数返回,
done
为真
浏览器(V8)实际是如何处理的?
根据 V8 官方的 blog,在 Node.js 12 的一系列优化后,async
函数在内部被转化成了
_17async function foo(v) {_17 const w = await v_17 return w_17}_17_17resumable 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,resume
和suspend
这些引擎内部函数与 generator 也有异曲同工之妙。performPromiseThen
在引擎的微任务队列中创建了PromiseReactionJob,与回调函数绑定。当主函数运行完,且await的promise resolve了之后,执行微任务,恢复foo函数的执行。