async/await 原理解析
相信大家都知道async/await
是基于 Generator
的而 async/await
又是到目前为止解决异步编程最优雅的方式了那么在 Generator
内部它是如何实现 "暂停"
然后又恢复执行的?
对于这个问题Generator
其实就是 JavaScript
语法层面上对协程的支持协程就是主程序和子协程直接控制权的切换并伴随通信的过程从 Generator
的角度来讲yield``next
就是通信接口next
是主协程向子协程通信两者相互交替。
在维基百科中有这样的定义:
协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。
其中 "允许执行被挂起与被恢复"
就是很好的解释协程可以通过 yield
(取其“让步”之义而非“出产”)来调用其它协程,接下来的每次协程被调用时,从协程上次 yield
返回的位置接着执行,通过 yield 方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。
接下来我们继续打开 Babel 官网 继续编译以下代码:
async function foo() {
const result1 = await 1;
const result2 = await 2;
return result1 + result2;
}
foo();
但是细心的你一定会发现这些代码和前面的 Generator
的代码几乎一毛一样所以这也就是为什么会说 async/await
的底层实现是基于 Generator
了详情请看下图:
async/await 实现
由于 async/await
是关键字我们并没有定义关键字的能力所以我们可以通过函数来模拟实现首先实现迭代处理函数:
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 形式
// 同一以 Promise.resolve() 的方法返回并且递归调用 next() 函数
// 直到 done === true 为止
Promise.resolve(value).then(_next _throw);
}
}
再来实现模拟异步函数:
function _asyncToGenerator(fn) {
return function () {
// this 指向全局
var self = this
args = arguments;
// 将返回值promise化
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);
});
};
}
最后通过一个案例进行测试完美通过:
const asyncFunc = _asyncToGenerator(function* () {
const e = yield new Promise((resolve) => {
setTimeout(() => {
resolve("e");
} 1000);
});
const a = yield Promise.resolve("a");
const d = yield "d";
const b = yield Promise.resolve("b");
const c = yield Promise.resolve("c");
return [a b c d e];
});
asyncFunc().then((res) => {
console.log(res); // ['a' 'b' 'c' 'd' 'e']
});
async/await 异常捕捉
在开始之前我们先来理清一个问题就是为什么要进行错误处理?
由于 JavaScript
是一个单线程语言假如不进行错误处理会导致代码直接报错而无法执行显然这不是我们想要的那么我们应该怎么去捕捉到这个错误呢async.await
本身并没有提供这个机制给我们进行处理那么接下来我们将讲解四个方案一步一步由浅入。
try…catch
最简单的办法自然是 try.catch
了具体代码如下所示:
async function foo() {
try {
var result = await Promise.reject(new Error(111));
} catch (e) {
console.log(e);
}
return result;
}
foo();
通过 catch
能清楚的捕获到错误具体到哪行:
但是有一个问题是每一个 await
都进行捕获吗还是部分进行捕获那又对哪部分进行捕获难道你知道哪段代码在接下来运行的时候会报错吗这显然是一个很大的问题但是 async/await
的错误又该如何捕获呢?
await-to-js
其实已经有一个库 await-to-js 已经帮我们做了这件事,我们可以看看它是怎么做的,它的源码只有短短十几行
,我们应该读读它的源码,学学它的思想
/**
* @param { Promise } promise
* @param { Object= } errorExt - Additional Information you can pass to the err object
* @return { Promise }
*/
export function to<T U = Error>(
promise: Promise<T>
errorExt?: object
): Promise<[U undefined] | [null T]> {
return promise
.then<[null T]>((data: T) => [null data])
.catch<[U undefined]>((err: U) => {
if (errorExt) {
const parsedError = Object.assign({} err errorExt);
return [parsedError undefined];
}
return [err undefined];
});
}
export default to;
to 函数返回一个值这个值是一个数组数组之中有两个元素。如果索引为 0
的元素不为空值说明该请求报错如果索引 0
的元素为空值说明该请求没有报错也就是成功。
那么这个 to
函数怎么使用呢由于我们已经把 await-to-js
的源码拷贝下来了那么我们就不引入库 就直接使用了具体示例如下:
async function foo() {
const [error result] = await to(Promise.resolve(1));
console.log(result); // 1
console.log(error); // null
const [error1 result1] = await to(Promise.reject("error错误了"));
console.log(result1); // undefined
console.log(error1); // error错误了
console.log("代码还能正常执行");
}
foo();
但是这个处理方法据说类似于 Golang
一直被吐槽的对象如今又移植到了 JavaScript
上那么还有什么方法进行捕获呢?
babel 插件
首先在自己的项目的 babel
文件中的 plugins
中添加 babel-plugin-await-add-trycatch 具体配置可参考下图:
使用了该插件之后在编译的过程中实际上就是给每一个 await
添加了 try...catch
语句我们通过具体的代码来尝试一下:
const handle = async () => {
const result = await Promise.reject('error');
console.log(result);
};
handle();
再通过浏览器查看打印的报错信息:
报错的信息包含了报错的文件路径报错的方法报错的具体原因通过该插件我们能通过该插件能对 async/await
进行错误处理那么还有没有更好的方法呢答案是有的。
addEventListener 全局捕获
我们可以通过 windwo.addEventListener
来捕获到 async/await
抛出的错误并在这个方法的回调中的 event
接收这个报错信息具体代码实现如下:
window.addEventListener("unhandledrejection" (e) => {
e.preventDefault();
console.log(e.reason);
});
在这个 reason
里就存在着这些报错信息其中包括报错的文件路径报错信息已经报错的具体行数详情请看如下示例:
window.addEventListener("unhandledrejection" (e) => {
e.preventDefault();
console.log(e.reason);
});
async function foo() {
const result = await Promise.reject(new Error(111));
return result;
}
foo();
具体的输出如下所示: