Async/Await 原理讲解

1,404 阅读5分钟

前一篇文章中我们讲到了 Promiseasync/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 方法会返回一个对象,里面是由两个值,valuedone 组成,这两个值,第一个表示当前函数的返回值,第二个表示当前生成器函数的执行状态。

上面的示例都是无参数的,下面我们来看一看有参数的生成器函数:

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 函数的值是一样的。

generatorasync/await

上面介绍了 generator 之后,我们来看看 async/awaitgenerator 的联系,以及它是怎么通过 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 的时候再 resolvePromise,那么我们将上面的这段代码修改一下:

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 可以将这些执行结果在函数内串联起来,并且异步执行。