基于Promise的异步处理虽然解决了基于callback的过多嵌套的问题,但是可读性也还是比较差,流程控制也不是特别方便。所以ES7提出了async函数完美解决了上述问题。探究其原理async/await实际上是对Generator的封装,是一个语法糖,只不过generator出现不久后就被async/await取代了。

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,通过 next()方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。

可以看出yield和async/await已经非常相似了,他们都提供了暂停执行的功能,但是二者又有以下几点不同:

  • async/await自带执行器,不需要手动调用 next()就能自动执行下一步
  • async函数返回值是 Promise 对象,而 Generator 返回的是生成器对象
  • await能够返回 Promise 的 resolve/reject 的值

我们对 async/await 的实现,其实也就是对应以上三点封装 Generator

自动执行

我们先来看一下,对于这样一个 Generator,手动执行是怎样一个流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* myGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}

const gen = myGenerator();
gen.next().value.then(val => {
console.log(val);
gen.next().value.then(val => {
console.log(val);
gen.next().value.then(val => {
console.log(val);
})
})
});
// 输出1,2,3

我们也可以通过给gen.next()传值的方式,让 yield 能返回 resolve 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* myGenerator() {
const ret1 = yield Promise.resolve(1);
console.log('ret1', ret1);
const ret2 = yield Promise.resolve(2);
console.log('ret2', ret2);
const ret3 = yield Promise.resolve(3);
console.log('ret3', ret3);
}

// 通过给gen.next()传值的方式,让 yield 能返回 resolve 的值
const gen = myGenerator();
gen.next().value.then(val1 => {
gen.next(val1).value.then(val2 => {
gen.next(val2).value.then(val3 => {
gen.next(val3);
})
})
});

// ret1 1
// ret2 2
// ret3 3

显然,手动执行的写法看起来既笨拙又丑陋,我们希望生成器函数能自动往下执行,且 yield 能返回 resolve 的值,基于这两个需求,我们进行一个基本的封装,这里async/await是关键字,不能重写,我们用函数来模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function run(gen) {
const g = gen(); // 由于每次gen()获取到的都是最新的迭代器,因此获取迭代器操作要放在step()之前,否则会进入死循环

// 封装一个方法,递归执行next()
function step(val) {
const res = g.next(val);
if (res.done) return res.value; // 递归终止条件
// Promise的then方法是实现自动迭代的前提
// 等待Promise完成就自动执行下一个next,并传入resolve的值
res.value.then(val => step(val));
}

// 第一次执行
step();
}

对于之前的例子,我们就可以这样执行了:

这样我们就初步实现了一个async/await

上边的代码只有五六行,但并不是一下就能看明白的,我们之前用了四个例子来做铺垫,也是为了让读者更好地理解这段代码。简单的说,我们封装了一个 run 方法,run 方法里我们把执行下一步的操作封装成 step(),每次 Promise.then()的时候都去执行 step(),实现自动迭代的效果。在迭代的过程中,我们还把 resolve 的值传入gen.next(),使得 yield 得以返回 Promise 的 resolve 的值.

返回 Promise & 异常处理

虽然我们实现了 Generator 的自动执行以及让 yield 返回 resolve 的值,但上边的代码还存在着几点问题:

  • 需要兼容基本类型:这段代码能自动执行的前提是yield后面跟Promise,为了兼容后面跟着基本类型值的情况,我们需要把yield跟的内容(gen().next.value)都用Promise.resolve()转化一遍
  • 缺少错误处理:上边代码里的Promise如果执行失败,就会导致后续执行直接中断,我们需要通过调用Generator.prototype.throw(),把错误抛出来,才能被外层的try-catch捕获到
  • 返回值是Promise:async/await的返回值是一个Promise,我们这里也需要保持一致,给返回值包一个Promise

改造后的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function run(gen) {
// 把返回值包装成Promise
return new Promise((resolve, reject) => {
const g = gen();

function step(val) {
// 错误处理
try {
const res = g.next(val);
if (res.done) return resolve(res.value);
// res.value包装为promise,以兼容yield后面跟基本类型的情况
Promise.resolve(res.value).then(
value => step(value),
// 抛出错误
err => g.throw(err)
);
} catch (e) {
return reject(e);
}
}

step();
});
}

测试结果如下:
image.png

到这里,一个基本的async/await就实现完成了,但是直到结尾,我们也不知道 await 到底是如何暂停执行的,有关 await 暂停执行的秘密,我们还要到 Generator 的实现中去寻找答案.

Generator的实现

例子如下:

1
2
3
4
5
6
7
8
9
10
function* foo() {
yield 1;
yield 2;
yield 3;
}

const gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

我们可以在babel官网找到其在ES5下的实现:

image.png

代码咋一看不长,但如果仔细观察会发现有两个不认识的东西 —— regeneratorRuntime.markregeneratorRuntime.wrap,这两者其实是 regenerator-runtime 模块里的两个方法,regenerator-runtime 模块来自 facebook 的 regenerator 模块.直接看源码是有点懵,接下来我们实现一下它的低配版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 生成器函数根据yield语句将代码分割为switch-case块,后续通过切换_context.prev和_context.next来分别执行各个case
function gen$(_context) {
while (1) {
// 取next并将next赋值给下一次执行的prev
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 'result1';
case 2:
_context.next = 4;
return 'result2';
case 4:
_context.next = 6;
return 'result3';
case 6:
case "end":
return _context.stop();
}
}
}

// 低配版context
const context = {
next: 0,
prev: 0,
done: false,
stop() {
this.done = true;
}
};

// 低配版invoke
const gen = function () {
return {
next () {
const value = context.done ? undefined : gen$(context);
const done = context.done;
return {value, done};
}
}
};

image.png

从中我们可以看出,Generator 实现的核心在于上下文的保存,这其实是一个状态机。函数并没有真的被挂起,每一次 yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样.

参考文献

-ES6 系列之 Babel 将 Generator 编译成了什么样子