背景
async/await是定义于ES2017
的异步语法,async/await语法大大提高了异步JS代码的可读性,帮助JS开发者进一步摆脱了臭名昭著的回调地狱。
包括Edge(15+),Chrome(55+),Safari/iOS Safari(11+)在内的主流浏览器都支持了这一语法。不过在平时的开发中我们通常会用Bable来对这一语法进行转换。
那么Babel会怎么转换与执行async/await呢?
示例代码
编写以下代码,并通过Webpack+Babel进行编译(vue cli 3默认配置,经过plugin-transform-async-to-generator
和regenerator-transform
转换):
const getThree = function() {
return Promise.resolve(3);
};
const getFour = function() {
return Promise.resolve(4);
};
const test = async function() {
console.log(1);
console.log(2);
const three = await getThree();
console.log(three);
const four = await getFour();
console.log(four);
console.log(5);
};
将得到如下的代码:
var test = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
var three, four;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log(1);
console.log(2);
_context.next = 4;
return getThree();
case 4:
three = _context.sent;
console.log(three);
_context.next = 8;
return getFour();
case 8:
four = _context.sent;
console.log(four);
console.log(5);
case 11:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return function test() {
return _ref.apply(this, arguments);
};
}();
接下来,将以以上代码为例,从运行时和babel转换阶段分别介绍async/await的奇幻漂流。
运行时
regeneratorRuntime.mark
regeneratorRuntime.mark
会把_callee
的prototype设置为Generator
:
exports.mark = function(genFun) {
if (Object.setPrototypeOf) {
Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
} else {
genFun.__proto__ = GeneratorFunctionPrototype;
if (!(toStringTagSymbol in genFun)) {
genFun[toStringTagSymbol] = "GeneratorFunction";
}
}
genFun.prototype = Object.create(Gp);
return genFun;
};
Generator.prototype有next、return、throw三个方法,这三个方法会分别以'next'、'return'和'throw'调用this._invoke(method, arg)
。这里的_invoke是什么之后会介绍。
regeneratorRuntime.wrap
regeneratorRuntime.wrap
的作用是生成一个Generator实例,返回的generator的_invoke方法是由_callee$生成的:
function wrap(innerFn, outerFn, self, tryLocsList) {
var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
var generator = Object.create(protoGenerator.prototype);
var context = new Context(tryLocsList || []);
generator._invoke = makeInvokeMethod(innerFn, self, context);
return generator;
}
这里innerFn就是_callee$,outerFn是_callee。(所以regeneratorRuntime.mark这步是非必要的?)
makeInvokeMethod
makeInvokeMethod
包装了innerFn(状态机)的执行流程控制,外部通过next
等方法操作状态机:
function makeInvokeMethod(innerFn, self, context) {
var state = GenStateSuspendedStart;
return function invoke(method, arg) {
...
context.method = method;
context.arg = arg;
while (true) {
...
if (context.method === "next") {
context.sent = context._sent = context.arg;
} else if (context.method === "throw") {
if (state === GenStateSuspendedStart) {
state = GenStateCompleted;
throw context.arg;
}
context.dispatchException(context.arg);
} else if (context.method === "return") {
context.abrupt("return", context.arg);
}
state = GenStateExecuting;
var record = tryCatch(innerFn, self, context);
if (record.type === "normal") {
state = context.done
? GenStateCompleted
: GenStateSuspendedYield;
if (record.arg === ContinueSentinel) {
continue;
}
return {
value: record.arg,
done: context.done
};
} else if (record.type === "throw") {
state = GenStateCompleted;
context.method = "throw";
context.arg = record.arg;
}
}
};
}
状态机(_callee$)的初始状态为GenStateSuspendedStart
。随后调用next
执行状态机。method
是next
之类的操作,arg
是参数,即初始的调用参数或状态机上一步的返回。context
是状态机的上下文,arg被赋值到context.sent
以供状态机获取,状态机的prev
和next
是控制状态机的跳转,初始都为0,在状态机中会自行修改。
当状态机return时,这里会判断状态机状态是否完成,状态机中的_context.stop()
会将context.done
设置为true。
如果原始代码里有await(yield),那状态机中就会生成对应的return代码将Promise返回出去。之后_asyncToGenerator
会保证异步流程的继续执行。
_asyncToGenerator
_asyncToGenerator
将Generator或状态机包装为Promise返回,同时通过asyncGeneratorStep
使得异步流程持续执行:
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);
});
};
}
asyncGeneratorStep
会驱动Generator或状态机不断前进,每次返回后,判断返回类型,如果是异步流程(Promise)则等待Promise有结果后继续处理:
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);
}
}
小结
为了更便于理解,这里简述一下执行流程:
-
- test方法被调用,实际调用的是
_asyncToGenerator
返回的function,这个function返回的是一个promise,同时调用asyncGeneratorStep
开始执行
- test方法被调用,实际调用的是
-
- asyncGeneratorStep首先调用generator的next方法,这里的generator的
_invoke
方法会执行状态机_callee
- asyncGeneratorStep首先调用generator的next方法,这里的generator的
-
- 状态机初次执行
case 0
,执行完后状态变成{ done: false, next: 4 }
并返回getThree()
- 状态机初次执行
-
- asyncGeneratorStep在
getThree()
完成后将结果3作为context.sent
继续调用状态机
- asyncGeneratorStep在
-
- 状态机从
case 4
执行,从context.sent
获取上个异步流程的结果
- 状态机从
-
- 重复4-5这样的步骤,知道执行完毕
以上,就是async/await装换后的代码的执行流程。
转换阶段
在我的webpack配置中(vue cli 3默认配置),async/await的转换经历了两个插件:plugin-transform-async-to-generator
和regenerator-transform
。
plugin-transform-async-to-generator
plugin-transform-async-to-generator的工作相对简单,让首先看下转换的结果:
var test = /*#__PURE__*/function () {
var _ref = _asyncToGenerator(function* () {
console.log(1);
console.log(2);
var three = yield getThree();
console.log(three);
var four = yield getFour();
console.log(four);
console.log(5);
});
return function test() {
return _ref.apply(this, arguments);
};
}();
原本的代码被转换为generator,async/await分别对应了*与yield,并最终通过_asyncToGenerator转换为Promise并完成代码异步流程的执行。
这里的逻辑比较直观,感兴趣的话可以自己阅读源码(需要先对AST、babel、acorn等有所了解)。
regenerator-transform
regenerator-transform完成的工作是将generator(或async)代码转换为状态机。
完整的代码涉及了各种语法节点例如switch case、for loop等的转换处理逻辑,这里将仅仅以示例代码中涉及的部分介绍大体的流程。
// visit.js
exports.getVisitor = ({ types: t }) => ({
Function: {
exit: util.wrapWithTypes(t, function(path, state) {
let node = path.node;
if (!shouldRegenerate(node, state)) return;
...
let emitter = new Emitter(contextId);
emitter.explode(path.get("body"));
...
})
}
});
visit.js添加了Function的访问器。shouldRegenerate
判断当前function节点是否为generator或async,如果不是,则不对节点做改动;如果是,则通过emitter.explode
将原代码段转换为状态机代码。
// emitter.js
Ep.explode = function(path, ignoreResult) {
...
if (t.isStatement(node))
return self.explodeStatement(path);
if (t.isExpression(node))
return self.explodeExpression(path, ignoreResult);
...
};
示例代码的代码体是一个BlockStatement
节点,因此进入explodeStatement
方法:
// emitter.js
Ep.explodeStatement = function (path, labelId) {
...
if (t.isBlockStatement(stmt)) {
path.get("body").forEach(function (path) {
self.explodeStatement(path);
});
return;
}
if (!meta.containsLeap(stmt)) {
self.emit(stmt);
return;
}
switch (stmt.type) {
case "ExpressionStatement":
self.explodeExpression(path.get("expression"), true);
break;
...
}
}
如前所述,根节点为BlockStatement
,因此方法将遍历其子节点,调用explodeStatement
方法。
示例代码中,根节点的每个子节点都是ExpressionStatement
,因此我们就跳过例如IfStatement
、WhileStatement
、SwitchStatement
等等各类节点的处理分析。
对于每个节点,将通过containsLeap
判断其是否需要特别处理:
// meta.js
{
YieldExpression: true,
BreakStatement: true,
ContinueStatement: true,
ReturnStatement: true,
ThrowStatement: true
}
如果不需要特别处理,则直接调用emit
方法:
// emitter.js
Ep.emit = function (node) {
...
this.listing.push(node);
};
例如示例中的console.log(1)
和console.log(2)
两行代码,都直接加入了列表,因此在转换后的代码中,这两行代码仍然是在一起的,并不需要插入return
或者_context.next =
等控制代码,原代码流程保持不变。
而对于await(已经转换为了yield)的代码行,将需要额外处理,加入状态机的控制代码:
Ep.explodeExpression = function(path, ignoreResult) {
...
function finish(expr) {
...
self.emit(expr);
}
if (!meta.containsLeap(expr)) {
return finish(expr);
}
switch (expr.type) {
...
}
}
这里依然是使用了meta.containsLeap来判断是否需要处理,如果不需要,则直接调用emit
我们来看进入explodeExpression
的代码行const three = await getThree()
,这是一个需要进一步处理的AssignmentExpression,处理方式是进一步处理赋值的左右部分:
case "AssignmentExpression":
if (expr.operator === "=") {
return finish(t.assignmentExpression(
expr.operator,
self.explodeExpression(path.get("left")),
self.explodeExpression(path.get("right"))
));
}
...
进一步分解这个语句:
const three
是一个Identifier,无需处理await getThree()
是一个需要进一步处理的AssignmentExpressiongetThree()
是一个CallExpression,无需进一步处理
case "YieldExpression":
after = this.loc();
let arg = expr.argument && self.explodeExpression(path.get("argument"));
...
self.emitAssign(self.contextProperty("next"), after);
let ret = t.returnStatement(t.cloneDeep(arg) || null);
ret.loc = expr.loc;
self.emit(ret);
self.mark(after);
return self.contextProperty("sent");
这里loc()
获取的是当前行号(当前代码块中的行号),也就是在示例代码转换结果中看到的case 4
和case 8
中的4和8。emitAssign
生成了_context.next = 4
这样的状态跳转代码,emit(ret)
这步生成了return getFour()
这些代码,用以将控制流程转回到_asyncToGenerator
。contextProperty("sent")
指明了异步流程返回值的获取方式four = _context.sent
。
mark
方法标记了行号,以便在节点转换完成后统一插入case
代码:
Ep.mark = function(loc) {
let index = this.listing.length;
...
this.marked[index] = true;
return loc;
};
Ep.getDispatchLoop = function () {
...
let alreadyEnded = false;
self.listing.forEach(function(stmt, i) {
if (self.marked.hasOwnProperty(i)) {
cases.push(t.switchCase(t.numericLiteral(i), current = []));
alreadyEnded = false;
}
if (!alreadyEnded) {
current.push(stmt);
if (t.isCompletionStatement(stmt)) alreadyEnded = true;
}
});
...
};
小结
至此,简单介绍了async/await语法在被babel插件处理成状态机语法的过程,以及运行时状态机的执行流程。
感兴趣的话可以阅读regenerator-transform的源码,了解各种语法在从异步语法转换为状态机过程中的流程。
谢谢您的阅读
:)