前言
在学习 JavaScript
的过程中,理解并灵活运用异步相关知识是一件不容易的事情,这体现在代码可读性、健壮性上,好在 ES6
出现后挽回了这一局面,我们不再需要编写可读性不高的回调嵌套,也不用为了代码的健壮性而处处小心,这得益于 Promise
与 async Function
,它们给我们带来了更优秀的异步方案,今天我们就来学习异步函数相关知识。
学习异步函数前,要求读者熟悉 Promise
与 生成器,如果未了解过,可以看看读者的文章
async Function
虽然 Promise
对于传统的异步编程而言已经足够优秀,但精益求精的前辈们仍觉不足,Promise
的代码执行顺序对于开发者而言依然不够直观,为次,ES6
又提出了异步函数的相关概念。
异步函数,即一个特殊函数,通过在函数声明前添加 async
关键字标识
async function () {}
// or
const asyncArrow = async () => {}
复制代码
在异步函数中,能够使用关键词 await
,它期待一个 Promise
值,并等待它决议,此时会暂停当前的程序并返回,控制权交还给主线程去执行其他任务
async function asyncFun() {
await Promise.resolve();
console.log('end');
}
asyncFun();
console.log('start');
复制代码
上述代码中的正确执行顺序是 start
、end
,这意味着函数调用后异步函数内的程序被暂停了,所以首先输出 start
,然后输出 end
,它的行为如下
调用异步函数
等待一个立即完成的 Promise
给立即完成的 Promise
添加一个微任务
返回
输出 start
微任务执行,asyncFun
程序恢复执行
await
虽然期待一个 Promise
值,但这不是必须的,它可以等待任何值,此时浏览器会将这个值包装为一个 Promise
async function asyncFun() {
await 1;
// 类似于
await Promise.resolve(1);
}
复制代码
await
还能够获取到 Promise
的完成值
async function asyncFun() {
const result = await Promise.resolve('fulfilled');
console.log(result);
}
asyncFun(); // fulfilled
复制代码
如果等待的 Promise
被拒绝,那么就会在程序停止的位置抛出异常,我们能够通过 try...catch
进行异常捕获
async function asyncFun() {
try {
await Promise.reject('出错了');
} catch (err) {
console.log(err); // 出错了
}
}
asyncFun();
复制代码
异步函数的返回值会被包装成一个 Promise
,如果没有显式的返回一个值,那么 Promise
完成值就是隐式返回的 undefined
async function asyncFun() {
return 'complete';
}
asyncFun().then((value) => {
console.log(value); // complete
});
复制代码
看到这里,其实异步函数的一些特性就讲完了,但是你有没有发现异步函数与生成器有很多相像的地方呢?
// async
async function asyncFun() {
await Promise.resolve();
}
// generator
function* generator() {
yield Promise.resolve();
}
复制代码
异步函数通过 async
来标识,而生成器通过在 function
关键字后添加 *
来标识,且它们都有各自的关键字能用于暂停程序的执行,但仅仅如此吗?看一下下面的代码
function* generator() {
const result = yield Promise.resolve('generator');
console.log(result); // generator
}
function run(exec) {
// 假设 exec 必定是一个生成器
// 获取生成器对象
const g = exec();
// 启动生成器,获取 yield、return 出的值
const p = g.next();
// 假设 p.value 必定是一个 Promise
p.value.then(
(value) => g.next(value),
(err) => g.throw(err)
);
}
run(generator);
复制代码
上述代码中,我们添加了一个 run
函数,用于控制生成器的执行,run
函数利用 yield
双向数据传递的特性,接受生成器返回的值,这里我们假定为 Promise
值,并给它添加一个 then
处理回调,当这个 Promise
完成时在成功回调中调用生成器对象的 next
方法并将完成值注入到上一个导致程序暂停的 yield
身上,如果失败则调用 throw
方法并注入失败原因。
现在你发现了吗?其实异步函数只是一个语法糖,基于 Promise
+ 生成器我们也能实现完全相同的功能,所缺的只是一个控制生成器执行流程的执行器而已。
实现执行器
我们已经了解了异步函数其实只是 Promise
+ 生成器的语法糖,但还缺少了一个关键的执行器函数,以下是摘抄自 《你不知道的 JavaScript》 的实现
function run(exec, ...args) {
// 调用生成器函数并传递参数
const it = exec.apply(this, args);
return Promise.resolve().then(function handleNext(value) {
// 不断处理 next 调用
const next = it.next(value); // 获取 next 返回值
// 立即执行函数,传递 next 返回值
return (function handleResult(next) {
if (next.done) {
// 执行完毕直接返回
return next.value;
} else {
// 未执行完毕则给 yield 出的 Promise 添加then处理回调
// 同时避免 yield 非 Promise 值,需要进行一层 resolve 包装
return Promise.resolve(next.value).then(
handleNext, // 成功回调,继续调用 handleNext 获取下一个值
function handleErr(err) {
// 失败回调
return Promise.resolve(
it.throw(err) // 注入异常,如果异常在生成器内部被处理则继续调用 handleResult
).then(handleResult);
}
);
}
})(next);
});
}
复制代码
拥有了这个执行器函数,我们就能够基于 Promise
+ 生成器来模拟异步函数的行为了
function* generator() {
try {
const result = yield Promise.reject('出错啦');
} catch (err) {
console.log(err); // 出错啦
}
const data = yield Promise.resolve('data');
console.log(data);
return 'complete';
}
run(generator).then((value) => {
console.log(value); // complete
});
// 出错啦
// data
// complete
复制代码
结语
本文讲述了异步函数的相关概念,并了解了异步函数其实只是语法糖,我们完全能够自己实现相同的功能,可以看到,生成器在这中间充当了非常重要的角色,以后可能还会基于生成器出现更多强大的模式。
参考资料
《JavaScript 高级程序设计》
《你不知道的 JavaScript》
评论