async与await的顿悟

async与await的顿悟

晚上下班回家的路上,一边翻看高程4一边琢磨await的效果,直到晚上突然顿悟了JS的异步模式

晚上下班回家的路上,一边翻看高程4一边琢磨await的效果,直到晚上突然顿悟了JS的异步模式。“单线程消息队列”这几个字天天看,就是没仔细想过是怎么回事,刚才一下都想清楚了。

async关键字

async关键字可以加在普通函数,函数表达式,箭头函数之前。加上之后,效果就是把原来函数的返回值用Promise.resolve()包裹起来作为返回值。如果函数没有返回值,则会包裹一个undefined

这种函数被称为异步函数,但要注意,其内部如果没有异步代码(then()await之类),那么这个函数执行过程还是同步求值的,看一个简单例子:

    async function test() {
        console.log("async function inner");
	return 42;
    }

    test().then((x) => console.log(x));

    console.log(1);
    console.log(2);
    console.log(3);

打印出的是

async function inner
1
2
3
42

这里的要点首先是异步函数内的代码实际上被同步执行了。之后的test()实际上相当于Promise.resolve(42),已经知道then()会放一条消息到异步队列,等主线程同步代码执行完毕再执行,所以就是这样的效果。

单独使用async其实就是快速创造一个Promise对象的语法糖,要搭配await来使用。

await关键字

感谢高程4,让我在研读await的过程中顿悟了JavaScript的异步模式。

await的作用是在那行代码等待Promise的状态落定,与async自动打包类似,await会自动解包,即等待那个Promise的解决值或者拒绝理由。一开始await的表现方式让我还摸不到头脑:

    async function test() {
        let p = new Promise((resolve, reject) => {
            setTimeout(resolve, 3000, 42);
        });
        await p;
        console.log("inner async function");
        return p;
    }

    test().then(x => console.log(x));
    console.log(3);

这一段打印出结果是:

3
(三秒钟之后)
inner async function
42

按照我原来的理解,await这里类似于Java中一个线程等待Future的结果一样,应该是阻塞的才对,为什么会先打印个3出来呢?
这就是我琢磨了半个晚上的问题,后来仔细看了高程4里关于实际执行的顺序,以及想想我上一篇博客,突然就明白了。那就是JavaScript还真的就是个主线程+一个异步消息队列组成的。像setTimeout这种函数,别管浏览器是不是真的另外开了一个线程去运行这个代码,对于我们JS代码来说,只要是异步代码,就是往消息队列里扔一个消息,而消息队列是要等到主线程也就是所有同步代码都执行完毕才会得到执行。所以我之前把Promise类比为Java中的Future,是完全错误的,Java那个是真的多线程调度,而JavaScript里就认为只有一个主线程,所有的异步代码都老老实实到队列里去排队,主线程绝对不会阻塞,一直哗哗的往下运行,就算运行到底了,整个程序也没结束,消息队列里的玩意再一个一个按照放进去的顺序得到执行。

那么是异步代码,setTimtoutsetIntervalthen()都是,另外就是这个await关键字,也是异步代码

所以这个时候我自己分析一下上边代码的执行顺序如下:

  1. test()执行,其中的代码同步执行,要创建一个3秒钟后解决值是42Promise对象
  2. 创建Promise对象,同时其中的执行器函数同步执行
  3. 执行器函数中的setTimeout向消息队列中放入一条执行自己的消息
  4. 执行器函数结束,创建Promise的这一行执行完毕
  5. await此时会向消息队列中放入一条等待期约落定的消息,然后会结束异步函数的运行(而不是阻塞在当前一直等待),也不是继续执行异步函数的剩余部分
  6. 程序从test()的调用中退出,添加then()中的处理方法
  7. then()向消息队列中放入一条消息
  8. 继续向下执行到console.log(3),同步代码全部结束,此时屏幕上打印了一个3,前边所有语句都没有输出。
  9. 到达3秒钟,最早进入消息队列的setTimtout执行,将Promise的设置为解决值42
  10. await等待期约落定的消息被执行,检查期约确实已经落定,将p的值设置为期约解决值42
  11. **异步函数从await中断的地方继续向下执行,打印出inner async function
  12. 执行return p语句,实际上返回了一个Promise.resolve(42)
  13. 消息队列中then()放入的消息得到执行,实际上是执行处理函数,打印出42

实际上异步队列的处理会更加复杂一些,比如之前提到过then()先添加后添加都一样。实际的异步更关心结果而不是执行顺序,所以这里要特别注意await带有跳出异步函数的功能

高程4中的打印123456789的例子经过实际实验,确实打印出的是123458967,await等待Promise只会放入一个异步消息了。

再比如高程4举例的sleep()函数,如果执行代码是:

    async function sleep(delay) {
        return new Promise((resolve, reject)=>{
            setTimeout(resolve, delay);
        })
    }

    async function codeToExecute() {
        console.log("code before sleep");
        let start = new Date();
        await sleep(3000);
        console.log(new Date() - start);
        console.log("code after sleep");
    }

    codeToExecute();
    console.log("主线程同步代码依然不受影响");

打印出的结果是:

code before sleep
主线程同步代码依然不受影响
(三秒钟后)
3012
code after sleep

异步函数内的代码睡眠了3秒钟,主线程的同步代码依然不受影响,会在await放入消息后立刻执行。

后记

await简单的说,就是等待期约落定,这个过程中不会阻塞主线程同步代码,只会把期约落定之后如何处理放到消息队列中,之后打断异步函数;最关键的是:之后等到消息队列中执行到这个消息,才会再往下执行异步函数后续的代码,所以阻塞效果仅限于异步函数内部的代码

然后发现,由于await必须要写在async函数中,不能直接使用,所以只会阻塞函数内部的代码,实际上没有什么东西能够阻止JavaScript的主线程同步代码.....

好了,把顿悟立刻记录下来了。后边关于Promiseasync/await的各个细节还需要继续研究。

LICENSED UNDER CC BY-NC-SA 4.0
Comment