面试官灵魂一问: 你知道 Babel 是如何编译 Async/Generator
的吗?
polyfill 后的代码
既然想知道其原理,那么自然是要看下 polyfill
后的代码的,直接到 Babel官网的REPL在线编辑器上,配置好 presets
和 plugins
后,输入你想要转化的代码,babel
自动就会给你输出转化后的代码了
以下述代码为例:1
2
3
4
5
6
7async function test1 () {
console.log(111)
await a()
console.log(222)
await b()
console.log(3)
}
babel
输出的代码是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 ;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
function test1() {
return _test.apply(this, arguments);
}
function _test() {
_test = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee() {
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log(111);
_context.next = 3;
return a();
case 3:
console.log(222);
_context.next = 6;
return b();
case 6:
console.log(3);
case 7:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _test.apply(this, arguments);
}
很明显,_test
函数中 while(1)
方法体的内容,是需要首先注意的代码
可以看出来,babel
把原代码进行了一次分割,按照 await
为界限,将 async
函数中的代码分割到了 switch
的每个 case
中(为了表述方便,下文将此 case
代码块中的内容称作 await
代码块),switch
的条件是 _context.prev = _context.next
,与 _context.next
紧密相关,而 _context.next
这个变量,会在每个非 case end
中被赋值,值就是原代码中被分割后的下一个将要执行的 await
代码块的内容,当原代码中的所有 await
被执行完毕后,会进入 case end
逻辑,执行 return _context.stop()
,代表 async
函数已经执行完毕
但这只是最基本的,代码到底是怎么串连起来的,还要继续往外看
下文讲解的源代码版本:”@babel/runtime”: “^7.8.4”
流程串连
首先,需要看下 _interopRequireDefault
这个方法:
1 | function _interopRequireDefault(obj) { |
代码很简单,如果参数 obj
上存在 __esModule
这个属性,则直接返回 obj
,否则返回一个属性 default
为 obj
的对象,其实这个主要就是为了兼容 ESModule
和 CommonJS
这两种导入导出规范,保证当前的引用一定存在一个 default
属性,否则没有则为其加一个 default
属性,这样便不会出现模块的 default
为 undefined
的情况了,就是一个简单的工具方法
然后继续看 _regenerator
,while(1)
这个循环体所在的函数,作为 _regenerator.default.wrap
方法的参数被执行,_regenerator
是从 @babel/runtime/regenerator
引入的,进入 @babel/runtime/regenerator
文件, 里面只有一行代码 :module.exports = require("regenerator-runtime");
,所以最终应该是 regenerator-runtime
库,直接找 wrap
方法
1 | function wrap(innerFn, outerFn, self, tryLocsList) { |
innerFn
是 _callee$
, outerFn
是 _callee
, outerFn.prototype
也就是 _callee.prototype
,_callee
也是一个函数,但是经过了 _regenerator.default.mark
这个方法的处理,看下 mark
方法
1 | exports.mark = function(genFun) { |
主要就是为了构造原型链,GeneratorFunctionPrototype
以及 Gp
又是什么呢?
1 | function Generator() {} |
还是构建原型链,最终如下:
所以,回到上面的 wrap
方法,protoGenerator
就是 outerFn
,也就是_callee
,generator
的原型链指向 protoGenerator.prototype
这里有个 context
实例,由 Context
构造而来在:
1 | function Context(tryLocsList) { |
主要看下 reset
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//...
Context.prototype = {
constructor: Context,
reset: function(skipTempReset) {
this.prev = 0;
this.next = 0;
// Resetting context._sent for legacy support of Babel's// function.sent implementation.this.sent = this._sent = undefined;
this.done = false;
this.delegate = null;
this.method = "next";
this.arg = undefined;
//...
},
//...
}
很明显,reset
方法的作用就和其属性名一样,是为了初始化一些属性,主要的属性有 this.prev
、this.next
,用于交替记录当前执行到哪些代码块了,this.done
,用于标识当前代码块是否执行完毕,先不细说,后面会提到
然后 generator
上挂载了一个 _invoke
方法1
2
3// The ._invoke method unifies the implementations of the .next,
// .throw, and .return methods.
generator._invoke = makeInvokeMethod(innerFn, self, context);
看下 makeInvokeMethod
的代码:
1 | function makeInvokeMethod(innerFn, self, context) { |
粗略来看,此方法又返回了一个方法,至于方法体里是什么,暂时先不管,继续往下看
_regenerator.default.mark(function _callee() {//...})
作为 _asyncToGenerator2.default
方法的参数执行,所以继续看 _asyncToGenerator2
:
1 | function _asyncToGenerator(fn) { |
_asyncToGenerator
同样返回了一个函数,这个函数内部又返回了一个 Promise
,这对应着 async
函数也是返回一个 promise
, 通过_next
调用 asyncGeneratorStep
:
1 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { |
参数 gen
其实就是上面提到过的 generator
,正常情况下,key
是 "next"
, gen[key](arg);
相当于 generator.next(arg)
, generator
上哪来的 next
属性呢?其实是通过原型链找到 Gp
,在Gp
上就存在 next
这个属性:
1 | // Helper for defining the .next, .throw, and .return methods of the |
这个的 this._invoke(method, arg);
,其实就是 generator._invoke("next", arg)
所以,现在再来看一下 makeInvokeMethod
方法返回的 invoke
方法,按照正常逻辑会走这一段代码:
1 | function tryCatch(fn, obj, arg) { |
执行 tryCatch
方法,返回了一个存在两个属性 value
、done
的对象,其中 tryCatch
的第一个参数 fn
,就是包含 while(1)
代码段的 _callee$
方法,这样,整个流程就串起来了
流程解析
在 while(1)
的循环体中,_context
参数就是 Context
的实例,上面提到过,_context
上的 prev
和 next
属性都被初始化为 0
,所以会进入 case 0
这个代码块,执行第一块 await
代码块,得到info
结果,判断 info.done
的值
1 | if (info.done) { |
保证原async
函数中所有 await
代码体全部执行完毕的逻辑就在此处
如果 info.done
不为 true
,说明 原async
函数中await
代码体还没有全部执行完毕,进入 else
语句,利用 Promise.resolve
来等待当前的 await
代码块的 promise
状态改变,然后调用 then
方法,通过执行 _next
方法来调用 asyncGeneratorStep
,继续执行 _callee$
,再次走 switch
代码段,根据更新后的 _context_prev
来指示进入下一个 case
,以此循环,当所有的 await
代码段执行完毕后,会进入 case 'end'
,执行 _context.stop();
这个东西
1 | Context.prototype = { |
stop
方法中,主要就是设置 this.done
为 true
,标识当前异步代码段已经执行完毕,当下次再执行 asyncGeneratorStep
的时候,进入:1
2
3if (info.done) {
resolve(value);
}
不再继续调用 _next
,流程结束
其实当时面试的时候,面试官问我 async/await
的实现原理,我第一反应就是 Promise
,但紧接着我又想到 Promise
属于 ES6
,polyfill
这个东西最起码也得是 ES5
啊,所以我又放弃了这个想法,万万没想到,还可以双层 polyfill
简易版实现
通过上述分析可知,Babel
对于 async/await
的 polyfill
其实主要就是 Promise + 自调用函数
,当然,前提是需要通过字符串解析器,将 async
函数的按照 await
为分割点进行切分,这个字符串解析器涉及到的东西比较多,比如词法分析、语法分析啦,一般都会借助 @babel/parser/@babel/generator/@babel/traverse 系列,但这不是本文的重点,所以就不展开了
假设已经实现了一个解析器,能够将传入的 async
函数按照要求分割成几部分
比如,对于以下源码:
1 | // wait() 是一个返回 promise 的函数 |
将被转化为:
1 | function test1 () { |
当然,这只是简易实现,很多东西都没有考虑到,比如 await
返回值啊,函数返回值啊等,只是为了体现其原理
for 循环?
当时面试的时候,当我滔滔不绝地说完了 异步函数队列化执行的模式 这个概念后,面试官可能没想到我居然在明知道自己是在猜的情况还能心态这么好地说了那么多,沉默了片刻后,似乎是想打压一下我嚣张的气焰,又问,如果是 for
循环呢,怎么处理?
类似于以下代码:
1 | async function fn1 () { |
当时我其实已经知道猜错了,但既然猜了那就猜到底,自己装的逼无论如何也要圆回来啊,于是继续用这个概念强行解释了一通
实际上当时我对于 for
循环的这个处理,思路上是对的,就是将 for
循环拆解,拿到 单次表达式;条件表达式;末尾循环体 这个三个表达式,然后不断改变 条件表达式,直到触发末尾循环体,babel
的处理结果如下:
1 | // 只看主体代码 |
这就揭示了 async/await
函数的一个特性,那就是它具备暂停 for
循环的能力,即对 for
循环有效
Generator?
既然看完了 async/await
的实现,那么顺便看下 Generator
对于下述代码:1
2
3
4
5
6
7function* generatorFn() {
console.log(111)
yield wait(500)
console.log(222)
yield wait(1000)
console.log(333)
}
Babel
将其转化为:
1 | ; |
这套路跟 async/await
一样啊,也是把原代码进行切分,只不过Generator
是按照 yield
关键字切分的,最主要的区别是,转化后的代码相对于 async/await
的来说,少了 _asyncToGenerator2
这个方法的调用,而这个方法其实是为了自调用执行使用的,这同时也是 async/await
和 Generator
的区别所在
async
函数中,只要await
后面的表达式返回的值是一个非Promise
或者fulfilled
态的 Promise
,那么async
函数就会自动继续往下执行,这在 polyfill
中的表现就是一个自调用方法
至于 Generator
函数想要在遇到 yield
之后继续执行,就必须要在外部手动调用 next
方法,而调用的这个next
,实际上在 async/await
的 polyfill
中就是由 _asyncToGenerator2
来自动调用的
除此之外,因为是手动调用,如果你不额外增加对异步 promise
的处理,那么 Generator
本身是不会等待 promise
状态变化的,之所以说 async/await
是 Generator
函数的语法糖,部分原因就在于 async/await
相比于 Generator
来说,已经内置了对异步 promise
的处理.