async/await不完全指北

485 阅读6分钟

导言:随着ES7的流行,一个特殊的函数async-await进入前端开发者的眼前,越来越多的前端er在手撸代码时遇上异步操作都喜欢用async/await函数去做处理,简简单单的两个关键字 async await 它们究竟是有什么样的魔法能够使反人类思考的异步过程整的服服帖帖,让程序的脚步符合咱们人类思考的一个流程呢?

async/await

async function test() {
    const res = fetch('https://xxxx.xx.cn');
    if (res.status === 200) {
        // do something
    }
}
test();

以上便是一段简单的async/await代码,用法及其简单,在async函数中做了一个异步请求,但是却用同步的写法去获取返回结果并对结果进行操作,处理逻辑和代码流程非常贴合人类常用的线性流程思维方式,第一步,第二步,第三步这样的流程;这跟我们一直熟知的ajax等异步请求有了质一样的不同,那么它是如何将这种异步请求转化为一个同步的过程呢?

生成器Generator

在了解async/await之前,首先需要认识一个叫做Genrator的家伙,因为async/await是在其基础上进化而来的。Generator引入了一个新的概念-协程。

协程

协程是一个比线程粒度更细的一个概念,用简单一点的话来说就是,协程算是线程里面的任务,大家都知道JS是一门单线程语言,那么该线程可以认为是一个父协程,线程里面的协程并不是并行的,每次进入协程,那么父协程将会暂停,所有的资源会让出给到当前进行的协程,所以对于协程来说,协程并不会消耗额外的资源。

如上图为例,一个线程里面有主要的父协程,当线程中有协程启动时,父协程让出资源权限,处于停止等待状态,协程A获取资源操作权限,直到协程A完成了操作之后,将结果返回给父协程,同理,协程里面也可以再启动子协程;

Generator

知悉了协程的概念后,可以看一看Generator是如何运行的呢?

function* test() {
    console.log('进入test协程');
    console.log(1);
    yield 200;
    console.log('进入test协程');
    console.log(2);
    yield 300;
    console.log('进入test协程');
    console.log(3);
    yield 400;
    console.log('进入test协程');
    console.log(4);
    return 500;
}

const testDemo = test();
console.log(testDemo.next().value);
console.log('回到父协程1');
console.log(testDemo.next().value);
console.log('回到父协程2');
console.log(testDemo.next().value);
console.log('回到父协程3');
console.log(testDemo.next().value);
console.log('回到父协程4');

在浏览器运行结果如下

  1. test函数创建一个test协程,在调用test函数的时候并不会立即进入test协程,此时控制权还在父协程;
  2. 调用testDemo对象的next方法,进入到test协程,此时父协程让出控制权,进入等待状态,test协程获得控制权,执行test函数里面的第一个yield前面的代码,并且将yield的结果返回到父协程,同时将控制权让出;
  3. 父协程获得控制权,执行 console.log('回到父协程1') 代码语句,而后下一个next方法再次进入到test协程,重复2的过程,直至return语句退出test协程,test协程撤销;

以上过程可以如下图所示

直到最终return 500 test协程完全执行完毕,之后就移交给父协程操作了。

async/await

说了那么多,那究竟async/await是怎么运行的呢?

先看下面一段代码

async function b() {    console.log(1);    var a = await 200;    console.log(a);    return 2;}console.log('start');new Promise((resolve) => {    resolve('promiseA');}).then(res => console.log(res));var c = b();console.log(c);console.log('end')new Promise((resolve) => {    resolve('promiseB');}).then(res => console.log(res));

以上代码是个什么样的执行状况呢,如下

start
1
Promise {<pending>}
end
promiseA
200
promiseB

首先先看

var c = b();
console.log(c);

这两行代码,根据输出结果来看,所谓的async 函数,它默认固定返回的是一个promise对象,即使你定义了它的返回值;那么await又是一个什么东西呢?

这次咱们先来看看整个代码的运行流程;

1、首先是输出start;

2、创建promise,把promiseA加入到当前宏任务中的微任务队列中;【promiseA】

3、执行函数b();函数b入栈,执行函数b里面的代码,输出1;

4、遇到await 200,这里暂时不知道做了什么,先看后续的输出先;

5、发现退出了函数b,其实这里就是之前讲的协程了,回到了父协程,我们可以理解为第4步,await 其实就是从协程b中将值200抛到父协程,然后暂停协程b的控制,返回到父协程;

6、输出end

7、添加promiseB微任务;【promiseA, promiseB】,当前宏任务执行完,开始执行微任务队列,应该输出 promiseA 然后再输出 promiseB,发现运行结果却是

promiseA
200
promiseB

200是哪里冒出来的?怎么中间就多了个200?

这 200 其实就是函数 b() 抛出来的,看到 第4步 跟 第5步,await 退出了当前协程b,进入父协程,这个过程其实相当于是一个new Promise的操作,async/await 本身进行了对应的处理,其实就是 await 200 --->  var _promise = new Promise ((resolve, reject) => resolve(200)) 同时将这个_promise抛给父协程,即如下

_promise.then((res) => {
    // 父协程获取到这个res后,父协程推出控制,将控制权交给协程b,并将res的值传给协程b
})

在这个过程中微任务队列发生变化,变成了【promiseA, _promise.then(200), promiseB】;

现在就明白为什么是输出

promiseA
200
promiseB

在执行微任务_promise.then(200)的时候,父协程退出控制,进入协程b,执行后续内容,直至return退出协程,最终又回到父协程,执行微任务队列中的剩余微任务 promiseB;

总结

其实 async / await 搭配使用就是相当于生成器 + 协程 + promise 三者统一起来,化异步为同步的写法,使其更符合人类的思考模型,在使用 async / await 时刻谨记 当前是在什么协程,什么时候退出子协程,回到父协程,当前await回去的微任务处于队列哪个位置,并且什么时候该父协程又通过这个微任务进入到子协程,这样才能更好的使用它,拥抱新的异步方式。

注:文章有理解不到位的地方,欢迎大佬指正!