实现 async/await
async/await 简单介绍
很多人其实很好奇为什么在 Promise 提出之后又提出了 async/await 语法?似乎他们解决的是同样的事情,都是为了避免”回调地狱”这个问题。事实上,Promise 虽然很大程度上解决了回调嵌套的问题,但实际上他还是有一个不足之处:
- 当链式调用很多的时候,Promise 可读性仍然不高;
- Promise 内部状态的转换是我们没有办法控制的,只能执行完是什么状态就是什么状态,执行过程对我们来说是和黑盒子一样;
- 当异步任务之间存在依赖关系的时候,promise 只能通过 then 的链式调用来实现,其可读性也不高;
所以 async/await 函数的出现就是为了解决这些问题,在开始之前我们先来看一看 async/await 的简单用法:
// 定义一个async匿名函数
(async () => {
const a = await Promise.resolve(1);
const b = await Promise.resolve(2);
const c = await Promise.resolve(3);
console.log(a, b, c); // 1 2 3
})();
async/await 实现
首先我们需要知道 async/await 其实是 Generator 的语法糖,也就是对 Generator(生成器)做的一个封装。Generator 也是 ES6 中出现的新语法,但是出现不久之后就被 async/await 取代了,实际用到的也比较少。那 Generator 到底是啥呢?
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。可以通过 yield 关键字,把函数的执行流挂起,然后通过 next()方法可以切换到下一个状态。
在阮一峰老师的ECMAScript 6 入门中也讲过,Generator 函数形式上就是一个普通函数,但是有两个特征。一是,function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态(yield 在英语里的意思就是“产出”)。Generator 使用方式如下(例子摘自《ECMAScript 6 入门》):
function* helloWorldGenerator() {
yield "hello";
yield "world";
return "ending";
}
let hw = helloWorldGenerator();
console.log(hw.next()); // { value: 'hello', done: false }
console.log(hw.next()); // { value: 'world', done: false }
console.log(hw.next()); // { value: 'ending', done: true }
console.log(hw.next()); // { value: undefined, done: true }
在上述例子中,一共调用了 4 次 next 方法,且 next 方法返回的是一个对象,其中包含 value 和 done 两个字段,value 就是当前 yield 表达式的值,而 done 属性则表示 Generator 生成的遍历器对象是否遍历完。第 1 次调用 next 方法是在 Generator 开始时就执行,遇到第一个 yield 表达式为止。后面每一次调用都是从上一次 yield 表达式停止的地方执行到下一个 yield 表达式,一直到 Generator 函数运行完毕。如果在函数运行完之后继续调用 next 方法,那返回值永远是{ value: undefined, done: true },因为已经没有 yield 语句了且 Generator 状态已经遍历完。
上面就是 Generator 的大致用法了,如果有不懂的可以去ECMAScript 6 入门再看看,加强理解。除了上述的用法之外,我们还可以通过 next()传参来让 yield 具有返回值:
function* helloWorldGenerator() {
console.log(yield "hello");
console.log(yield "world");
console.log(yield "ending");
}
let hw = helloWorldGenerator();
hw.next();
hw.next("test1");
hw.next("test2");
hw.next("test3");
// test1
// test2
// test3
咋一看,Generator 函数和 async/await 函数似乎很接近,他们都提供了暂停执行的功能,但是二者也还是有区别的,我们来做一下测试:
async function helloWorldAsync() {
const a = await Promise.resolve(1);
const b = await Promise.resolve(2);
const c = await Promise.resolve(3);
console.log(a, b, c);
}
console.log(helloWorldAsync()); //Promise { <pending> }
helloWorldAsync(); // 1 2 3
function* helloWorldGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
let hw = helloWorldGenerator();
console.log(hw.next()); // { value: Promise { 1 }, done: false }
console.log(hw.next()); // { value: Promise { 2 }, done: false }
console.log(hw.next()); //{ value: Promise { 3 }, done: false }
console.log(hw.next()); //{ value: undefined, done: true }
由上述代码可以看出 Generator 函数和 async/await 函数的一些区别:
- async/await 可以自动执行下一步,而 Generator 函数则是需要手动调用 next()才能执行下一步;
- async 函数的返回值是 Promise 对象,而 Generator 函数的返回值是生成器对象;
- await 能够返回 Promise 的 resolve/reject 的值;
所以 async/await 对 Generator 的封装也是明确基于上述三点的封装:
自动执行
首先我们来看一下,如果要得到 Generator 函数的最后结果利用手动执行应该怎么做:
function* helloWorldGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
let hw = helloWorldGenerator();
hw.next().value.then((res1) => {
console.log(res1);
// 只有在上一步获得结果之后才执行下一步
hw.next().value.then((res2) => {
console.log(res2);
hw.next().value.then((res3) => console.log(res3));
});
});
// 1 2 3
这种方法写起来简直太麻烦了,和之前的多层回调嵌套没什么区别,所以我们肯定希望这个函数能自己往下执行,并且 yield 能够返回 Promise 对象 resolve 的值而不是返回 Promise 对象还要我们自己去调用 then 方法,所以我们先要对生成器函数进行封装:
function run(generator) {
let gen = generator(); // 获取迭代器
// 封装方法执行next(),因为可以通过next()给Generator函数传值,所以这里也设置了形参val
function _next(val) {
let res = gen.next(val);
// next()返回的值是对象{value:xxx,done:true/false}
if (res.done) return res.value;
// 如果遍历没有结束,就继续执行
// 加一个判断,处理yield后面不是Promise的情况
if (res.value instanceof Promise) {
res.value.then((res) => {
_next(res);
});
} else {
_next(res.value);
}
}
_next();
}
// 测试一下
function* helloWorldGenerator() {
const a = yield Promise.resolve(1);
const b = yield 2;
const c = yield Promise.resolve(3);
console.log(a, b, c);
}
run(helloWorldGenerator); // 1 2 3
上述代码看起来并不多。主要就是封装了一个 run 函数,函数里面又封装了一个_next 方法来实现自动迭代的效果。如果 yield 后面跟的是 Promise,我们就获取到 Promise 的 resolve 的值传入下一次迭代,使得 yield 可以返回 Promise 的 resolve 值。
错误处理
我们上面的代码虽然能够实现自动执行,但是还有一些小问题需要修复:
1、应该兼容基本类型:也就是说存在 yield 后面跟的不是 Promise 的情况,这种情况我们上面做过简单的处理,在这里再继续优化一下;
2、缺少错误处理:async/await 函数在 Promise 实行失败也就是 Promise 返回 reject 值是会抛出错误导致后续执行直接终中断,因此需要将错误抛出让外层 try/catch 能够捕捉到;
3、返回值是 Promise:之前说过 async/await 的返回值是一个 Promise,所以我们这里应该也要一致;
function run(generator) {
return new Promise((resolve, reject) => {
let gen = generator(); // 获取迭代器
// 封装方法执行next(),因为可以通过next()给Generator函数传值,所以这里也设置了形参val
function _next(val) {
// 错误处理,如果Promise执行出错,直接返回reject值
let res;
try {
res = gen.next(val);
} catch (error) {
return reject(error);
}
// next()返回的值是对象{value:xxx,done:true/false}
if (res.done) return resolve(res.value);
// 如果遍历没有结束,就继续执行
// 加一个判断,处理yield后面不是Promise的情况
// 把这里的判断改掉,直接将用Promise.resolve包起来,来兼容基本类型的情况
Promise.resolve(res.value).then(
(res) => _next(res),
(err) => {
// 如果出错就抛出错误,用于外层try/catch捕获
gen.throw(err);
}
);
}
_next();
});
}
// 测试
function* helloWorldGenerator() {
try {
console.log(yield Promise.resolve(1)); // 1
console.log(yield Promise.resolve(2)); // 2
console.log(yield Promise.reject("error")); // error
} catch (error) {
console.log(error);
}
}
run(helloWorldGenerator);
小结
到这里我们就算是实现了 async/await 了,但是事实上我们却没有研究 await 是如何实现暂停机制得,这个问题我们放在下篇文章中再进行研究~F