前一篇文章中我们讲到了 Promise 和 async/await 的基本用法,Promise 的原理还是比较清晰的,我们在这里就继续学习一下 async/await 的原理。
如果要学习 async/await 的实现,首先要知道一个最基础的概念,generator 生成器对象。
generator 生成器对象
在 JavaScript 中,使用
function* test() {}
这种方式声明一个函数的叫作生成器函数。调用生成器函数的时候会先保持一个暂停的状态,当调用 next 函数的时候,这种暂停的状态会被打破,继续执行函数,直到遇到 yield 语句。当 yield 语句执行完之后,函数又会进入暂停状态,直到下一次调用 next 函数。
这种函数和普通函数的区别在以下几点:
1.可以中断执行
我们知道,在 js 的函数的执行中,是无法中断的。但是生成器函数可以实现这一点:
function* test() {
yield 10;
yield 20;
}
let result = test();
console.log(result.next().value);
console.log(result.next().value);
// 输出
10
20
在上面这段代码中我们可以看到这个函数可以被分割成两段执行,这两段分别输出两个 yield 的值。而在这其中,你也可以选择第一个而不执行第二个函数。
2.会执行到 yield 语句为止
在生成器函数中,函数的执行会一直到 yield 语句,然后才会执行到 next 函数并输出结果, 但是 return 语句除外
function* test() {
return 20;
yield 10;
}
let result = test();
console.log(result.next().value);
// 输出:
20
3.next 方法返回的内容
next 方法会返回一个对象,里面是由两个值,value 和 done 组成,这两个值,第一个表示当前函数的返回值,第二个表示当前生成器函数的执行状态。
上面的示例都是无参数的,下面我们来看一看有参数的生成器函数:
function* test() {
x = yield 10;
yield x;
}
let result = test();
console.log(result.next().value);
console.log(result.next(100).value);
// 输出结果
10
100
在第二个表达式中,100 这个数字会被赋值给上一个 yield 语句中的左边的值。因此,我们在执行第二个 yield 语句后,会输出我们之前传入的参数值。
生成器函数本身也可以传入参数
function* test() {
let index = arguments[0] || 0;
while(true)
yield index++;
}
let result = test(5);
console.log(result.next().value);
console.log(result.next().value);
// 输出结果
5
6
在这个函数中,我们使用 arguments 来取到对应的参数,并连续输出两个 yield 语句的加法执行之后的值。
那么,如果说 yield 函数后面跟着的还是一个生成器函数,会有怎样的结果呢。我们看一下下面这个例子:
function* anotherGenerator(i) {
yield i+1;
yield i+2;
yield i+3;
}
function* generator(i) {
yield i;
yield* anotherGenerator(i);
yield i + 10;
}
let result = generator(5);
console.log(result.next().value);
console.log(result.next().value);
console.log(result.next().value);
console.log(result.next().value);
console.log(result.next().value);
// 输出
5
6
7
8
15
在这其中,我们可以看到当这个函数中执行到生成器函数的时候,它会将执行权交给 anotherGenerator 这个生成器函数,当这个生成器函数中的所有 yield 语句全部都执行完之后,才会回来继续执行接下来的语句。这个语法和 async/await 中如果 async 函数中有调用另外一个 async 函数时执行权会交给另外一个 async 函数的值是一样的。
generator 和 async/await
上面介绍了 generator 之后,我们来看看 async/await 和 generator 的联系,以及它是怎么通过 generator 实现的。
首先,我们声明一个简单的 async 函数
async function test() {
await 20;
}
test();
下面,我们就讲一下使用 generator 是如何实现这个函数的。我们在上一篇文章中讲到,async 函数最终都会返回一个 Promise,那么如果我们实现一个 async 函数,最外层也应该返回一个 Promise。
function wrapper() {
return new Promise((res, rej) => {
});
}
除此之外,我们还需要将上面的逻辑使用生成器函数实现,并返回最终的执行结果,这个结果需要在 wrapper 中使用:
function wrapper(test) {
return new Promise((res, rej) => {
});
}
function* test() {
yield 20;
}
wrapper(test);
通过这种方式,我们就可以将生成器函数最终取得的结果在 wrapper 函数中拿到,并在这其中使用:
function wrapper(test) {
return new Promise((res, rej) => {
const result = test().next();
try {
result
} catch(e) {
rej(e);
}
if(result.done) {
return res(result.value);
}
});
}
function* test() {
yield 20;
}
wrapper(test);
在这段代码中,我们通过传入的函数获取到了当前生成器函数的结果,并将它赋值给 result。通过 result 来检测当前的任务执行情况,并将返回值通过 Promise 传回函数中。
但是在这段代码中还有一个问题,由于我们的执行是异步的,那么当前任务有可能正处于 pending 状态。也就是说我们要等到 done 这个值为 true 的时候再 resolve 掉 Promise,那么我们将上面的这段代码修改一下:
function wrapper(test) {
return new Promise((res, rej) => {
const result = test();
function getState(state) {
const a = state();
try {
a
} catch(e) {
return rej(e);
}
if(a.done) {
return res(a.value);
}
Promise.resolve(a.value).then(function(v){
getState(function(){ return result.next(v) });
}, function(e){
getState(function(){ return result.throw(e)});
});
}
getState(() => result.next());
});
}
function* test() {
yield 20;
}
wrapper(test);
在这里,我们处理了既不是 reject,也不是 resolve 的情况,也就是 pending,等待中。这里我们使用了递归的方法,不断地去调用 getState, 从而获取到最终的执行结果。
上面就是 async/await 的实现方法。我们之所以不直接使用生成器函数而是使用 Promise 和生成器函数结合的方式,就是因为生成器函数只能在外部获取到其中某一个任务的执行结果,但是使用 Promise 可以将这些执行结果在函数内串联起来,并且异步执行。