一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
在《塞尔达传说·旷野之息》中,如果遇到较强的对手我们往往应该优先考虑服用三倍攻击料理。来吧,也尝尝啸达同学新鲜出炉的async/await三攻料理!!!
观看指南
前言
上一篇中我们介绍了async...await...
中的神奇魔法 —— 协程。文中最后的地方我提到了业界关于Nodejs是否支持协程这件事展开了激烈的讨论。我就用其中的一则示例展开我们今天的学习。
async function foo() {
await bar();
return 42;
}
async function bar() {
await Promise.resolve();
throw new Error('Error');
}
foo().catch(error => {
console.log(error.stack);
});
先看一下上述代码,这段代码没什么特殊意义,就是想打印一下异常的调用栈。我当前使用的开发电脑上node的版本是v16.11.1。我先尝试通过自己的机器运行一下这段代码:
紧接着,我有一台虚机,之前为了调测装了一个v8.10.0版本的NodeJS。我们用这台机器执行一下同一段代码:
首先这里明确两点:1. 异常肯定是bar
中抛出的;2. 两个版本最后展现的形式虽然不同,但是try...catch...
最终都成功地捕获到了异常。
当在16+版本环境执行的时候,异常堆栈中出现了 foo
的影子;而在8+版本中,异常堆栈中没有foo
的调用信息。这就让我们明显感觉到,不同版本NodeJS对async...await...
的支持是不一样的。上一篇讲的协程是ES6之后的产物,自然对应的是高版本里才有的东西。那低版本,也就是ES5中是怎么处理async...await...
的呢?
导航3
- 为什么
try...catch...
不能捕获异步异常 - 简介Promise解决方案
- 进程、线程、协程,傻傻分不清楚
- show me your code 👈👈👈
测试代码
废话不多说,先上原材料。测试代码比较简单,主要目的是要测试async...await...
的,同时在异步逻辑中尝试抛出异常,看看try...catch...
是怎么捕获到异常的。
async function test() {
try {
const result1 = await "result1";
const result2 = await timer(true);
const result3 = await timer(false);
console.log(result1, result2, result3);
} catch (e) {
console.log("err: ", e);
}
}
let timer = (success) => {
// 模拟一个简单的异步操作
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve("success");
}
reject("fail");
}, 1000);
});
};
test().then((res) => console.log(res));
babel一下
接下来,我通过Babel工具将代码转换成ES5代码:
"use strict";
// 这里其实是个polyfill,我自己加进去的,跟Babel没关系
const regeneratorRuntime = require("./regenerator-runtime/runtime")
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);
}
}
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);
});
};
}
function test() {
return _test.apply(this, arguments);
}
function _test() {
_test = _asyncToGenerator(
/*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
var result1, result2, result3;
return regeneratorRuntime.wrap(
function _callee$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.prev = 0;
_context.next = 3;
return "result1";
case 3:
result1 = _context.sent;
_context.next = 6;
return timer(true);
case 6:
result2 = _context.sent;
_context.next = 9;
return timer(false);
case 9:
result3 = _context.sent;
console.log(result1, result2, result3);
_context.next = 16;
break;
case 13:
_context.prev = 13;
_context.t0 = _context["catch"](0);
console.log("err: ", _context.t0);
case 16:
case "end":
return _context.stop();
}
}
},
_callee,
null,
[[0, 13]]
);
})
);
return _test.apply(this, arguments);
}
var timer = function timer(success) {
// 模拟一个简单的异步操作
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (success) {
resolve("success");
}
reject("fail");
}, 1000);
});
};
test().then(function (res) {
return console.log(res);
});
代码有点多,各位老铁先别慌,我们一点一点分析
_callee$()方法
我们先看看_test()里面都干了啥,把这里面的东西吃透了,代码就看明白了。我们从内往外扒,最内层函数为_callee$
。从内容上看,这块其实就是一个状态表。很多人应该做过性格测试,这个状态表就类似那种性格测试:
题目1:选出你第一时间看到的颜色
A. 红色 —— 请转到第3题
B. 蓝色 —— 请转到第5题
C. 绿色 —— 请转到第7题
D. 啥也没看到 —— 眼瞎,鉴定完毕!
function _callee$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.prev = 0;
_context.next = 3;
return "result1";
case 3:
result1 = _context.sent;
_context.next = 6;
return timer(true);
case 6:
result2 = _context.sent;
_context.next = 9;
return timer(false);
case 9:
result3 = _context.sent;
console.log(result1, result2, result3);
_context.next = 16;
break;
case 13:
_context.prev = 13;
_context.t0 = _context["catch"](0);
console.log("err: ", _context.t0);
case 16:
case "end":
return _context.stop();
}
}
}
regeneratorRuntime.wrap
接下看下wrap函数,wrap函数相当于给“性格测试”外面做了一层包装。
regeneratorRuntime.wrap(
function _callee$(_context) {
...
},
_callee,
null,
[[0, 13]]
);
首先介绍一下regeneratorRuntime模块,regeneratorRuntime模块来自facebook的regenerator模块,源码在这里。其作用就是给Generator
做语法兼容的,你可以把它理解为Generator
的Polyfill。这个代码有大几百行,这里不会把每一行都解释到,只会分析部分核心代码。
function wrap(innerFn, outerFn, self, tryLocsList) {
// 此处有省略...
var context = new Context(tryLocsList || []);
// makeInvokeMethod(innerFn,self,context)函数
// 参数innerFn待控制执行的函数,self为innerFn执行时的上下文,context为控制innerFn条件分支执行状态的操纵对象
// 返回invoke函数,触发innerFn函数以特定的条件分支执行,或报错,或终结生成函数
generator._invoke = makeInvokeMethod(innerFn, self, context);
return generator;
}
wrap
函数里最主要的动作就是绑定了makeInvokeMethod函数,makeInvokeMethod函数负责根据状态表进行状态之间切换的。就像那个性格测试一样,当你选择红色时,makeInvokeMethod会记录你当前的选择;从状态表中检查如果你选了红色,那下一题应该是哪一题?同时引导你开始答下一题:
// innerFn就是上文的_callee$,就是那个“性格测试”
function makeInvokeMethod(innerFn, self, context) {
// 设置开始状态
var state = GenStateSuspendedStart;
// method是当前需要执行的动作:throw | return | next
return function invoke(method, arg) {
// 此处有省略...
context.method = method;
context.arg = arg;
while (true) {
// 此处有省略...
if (context.method === "next") {
// Setting context._sent for legacy support of Babel's
// function.sent implementation.
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;
// innerFn是状态表函数,通过执行状态函数找出当前应该下一个状态,同时返回当前状态的结果
var record = tryCatch(innerFn, self, context);
if (record.type === "normal") {
// 此处有省略...
return {
value: record.arg,
done: context.done
};
} else if (record.type === "throw") {
state = GenStateCompleted;
context.method = "throw";
context.arg = record.arg;
}
}
};
}
regeneratorRuntime.mark
regeneratorRuntime.mark会把_callee的prototype设置为Generator。Generator.prototype有三个方法,分别是next
,return
,throw
。这三个方法的执行逻辑会在regeneratorRuntime.wrap
中实现。
exports.mark = function(genFun) {
if (Object.setPrototypeOf) {
Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
} else {
genFun.__proto__ = GeneratorFunctionPrototype;
define(genFun, toStringTagSymbol, "GeneratorFunction");
}
genFun.prototype = Object.create(Gp);
return genFun;
};
_asyncToGenerator && asyncGeneratorStep
如果研究过tj大神的co模块的,应该对下面代码有似曾相识的感觉。_asyncToGenerator
与co
十分类似,也是一个Generator的自动执行器。_asyncToGenerator
中先通过执行_next
触发了自动执行机。当状态机返回结果后,asyncGeneratorStep
会将结果包装成Promise,同时将_next
和_throw
当作这个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);
}
}
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);
});
};
}
异常是怎么捕获到的
其实从编译后的结果我们不难发现,我们在源代码中的try...catch...
被编译成一个奇怪的数组[[0, 13]]
。这个数组就代表源代码try...catch...
的作用域。[[0, 13]]
就代表case0 ~ 13之间的异常都需要被try...catch...
进行处理。
这里让我们简单跟着代码逻辑捋一下:
- 当执行到case9时,这时返回一个
timer(false)
的Promise,同时将next指针指向9 - asyncGeneratorStep在处理这个Promise时,发现Promise的状态被改为Rejected,于是执行_throw方法
- makeInvokeMethod方法执行throw逻辑时会检查
[[0, 13]]
数组,发现prev=6是处于[0, 13]这个范围之内的,则修改next为13 - 状态机转去执行13的逻辑,打印异常。
图例
我把测试代码的整个运行逻辑结合代码画了一下,我相信借助这个图可以更好地帮大家理解整个运行过程,从而更好的理解源码。
总结
经过一番分析不难发现,async...await...
在ES5中是通过Promise实现的。Promise对异步的处理我在跨越时空的等待之一中已经讲过了。到此,三篇的内容就此形成一个闭环,我自认为已经尽自己最大的努力把async...await...
中异步异常的捕获的内容讲清楚了。
还记得在第一篇中介绍背景知识的时候,我把主函数和异步逻辑比作平行时空下的你和ta,感谢async...await...
将宇宙打通,让我们有了跨越时空的能力、感知彼此的动力。所以async...await...
承载的不仅仅是对异步的等待,某种程度上也给人以美好的寄托,告诉我们有些人值得等待,有些美好终将到来!