前言

标题党了一点,但是不耽误你巩固事件循环的知识呀。注意是巩固,本文有几点易混的地方,不适合对事件循环一点都不懂的新手,不然看完可能就傻了。

再次警告???: 看完你可能更糊涂,虽说这也达到了我的效果

代码

function test(){
  console.log(0);

  setTimeout(function() {
    console.log(1);
    
    async function async1() {
      await async2()
      console.log(2)
    }
    async function async2() {
      console.log(3)
    }
    async1()
    new Promise(function(resolve) {
      console.log(4)
      resolve()
    }).then(function(){
      console.log(5)
    })
  })

  new Promise(function(resolve) {
    console.log(6);
    resolve();
  }).then(function() {
    console.log(7)
  }).then(function() {
    console.log(8)
  })

  setTimeout(function() {
    new Promise(function(resolve) {
      console.log(9);
      resolve();
    }).then(function() {
      console.log(10)
    })
  })
  return console.log(11)
}
test()
复制代码

上述代码总共有12个console.log,涉及到了关于async,await的一个易混点,还有本文最想告诉你的几个事件循环的知识点。

首先很明显的是,这段代码可以拆分成五个部分来看,一个console.log(1),两个setTimeout,一个new Promise,以及一个return。我们都知道,第一个输出的肯定是"0",因为他在test()调用的第一个宏任务----也就是运行这段函数代码的JS主线程上。剩下来的分析却要从三个方面来讲:

new Promise()

?第一个先讲最基本的,中间那段的new Promise():

  new Promise(function(resolve) {
    console.log(6);
    resolve();
  }).then(function() {
    console.log(7)
  }).then(function() {
    console.log(8)
  })
复制代码

很明显,这个Promise后面没有同步的console.log代码了。按照我们熟悉的,两个setTimeout都会进入宏任务队列等待,所以我们暂且不看。前面的0已经打印了,紧跟着是6也是没有疑问的(有疑问请出门右转找高赞的事件循环文章),那么接下来呢?

照理说只有两种可能,一种是打印7,因为这是第一个进入微任务队列的回调;另一种是打印11,因为这是目前JS主线程运行的唯一函数的返回值。因此问题又回到了是微任务队列清空之后函数返回呢?还是主进程清空之后再循环微任务队列呢?

答案是后者,主进程不空闲,即test函数不返回的话,微任务队列是不会循环的。即使你在浏览器的控制窗口看见一个很有意思的事:

在这里插入图片描述
其中undefined指的是函数返回值,尽管它出现在了微任务队列执行结束之后,但是11是毫无疑问的出现在了微任务队列循环之前.

关于这段如果不理解,你还可以去这个视频看这段关于事件循环的演讲。作者在最后五分钟举例的点击按钮和JS按钮执行的API对同一段代码不同执行顺序的讲解。

剩下的就没什么好说了,毕竟上面的图都暴露了,在我们执行第一个then的时候,第二个then又会进微任务队列,而直到微任务队列清空,主进程才回去宏任务队列拿下一个宏任务

所以前几个的顺序是:

0 -> 6 -> 11 -> 7 -> 8
复制代码

第一个setTimeout

?第一个setTimeout中有一个我们容易出错却又不怪我们的问题:

setTimeout(function() {
    console.log(1);
    
    async function async1() {
      await async2()
      console.log(2)
    }
    async function async2() {
      console.log(3)
    }
    async1()
    new Promise(function(resolve) {
      console.log(4)
      resolve()
    }).then(function(){
      console.log(5)
    })
  })
复制代码

首先我们抛开setTimeout的外壳,执行顺序是:

1 -> 3 -> 4 -> 5 -> 2
复制代码

这时候就引出了第一个争议点,因为你可以把这段代码拷到控制台,打印的结果却是:

1 -> 3 -> 4 -> 2 -> 5
复制代码

造成这个结果的原因是Node8中的一个错误,然后后来,开发人员发现后面这种输出结果其实更符合我们的需要,毕竟这种结果更直观。所以他们开始尝试将这个特性代入正规规范中,也就有了"--harmony-await-optimization"。

而从从V8 v7.2和Chrome 72开始,这种优化已经成为正统。也就是说,虽然我们按照await的标准,当第二次执行到await且await后面跟着的是Promise时,await只是会resolve放去微任务队列中,并在下一次微任务队列循环时才会再度执行await语句后面的语句。但我们要知道,这种标准最终屈服于await的性能优化。使得我们最终看到的都是:

1 -> 3 -> 4 -> 2 -> 5
复制代码

第二个setTimeout

☕上面那段第一个setTimeout的分析,要知道我们是独立于这个函数来讲解的,那么和第二个setTimeout结合起来的话输出顺序又会是怎么样呢??‍♂️

其实这个问题也可以转化为小问题去解决,我们现在纠结的无非是两个setTimeout是一个执行完再执行另一个呢?还是按照微任务队列的顺序交替执行呢?

其实这个答案我在第一部分就已经讲了,当我们微任务队列清空之后,主线程才会从宏任务队列里拿第一个setTimeout,而毫无疑问这个setTimeout执行过程中又会往微任务队列推新的Promise.resolve。同时我们又知道了如果JS主进程不清空微任务是不执行的。换句话说,只要主进程有东西,微任务队列也有东西,微任务队列的东西会一直执行直到空。

这部分刚刚贴的视频也有详细讲解。虽然有些人看我讲的就差不多懂了,但我还是推荐你去看一下这个视频

讲到这儿,这个题的答案也就出来了?:

0 -> 6 -> 11 -> 7 -> 8 -> 1 -> 3 -> 4 -> 2 -> 5 -> 9 -> 10
复制代码

理解若有错误欢迎指出,感谢?