JavaScript中的宏任务和微任务

先来个例子

如果能很快知道执行的顺序结果,那么说明你对这块的内容理解非常深刻。

<div class="parent" data-spm="2.2.2.2">
    <div class="child">123</div>
</div>
<script>
   var parent =  document.getElementsByClassName('parent')[0];
   var child =  document.getElementsByClassName('child')[0];
   parent.addEventListener('click',function (e) {
       console.log('parent')
   },true);
   child.addEventListener('click',function (e) {
       console.log('child')
   })
   new MutationObserver(function() {
       console.log('mutate 属性改变');
   }).observe(parent, {
       attributes: true
   });
   child.click();
   setTimeout(function () {
       console.log('定时器0执行了');
   },1000)

  setTimeout(function () {
      console.log('定时器1执行')
  })
   setTimeout(function () {
       console.log('定时器2执行')
   })
   new Promise(function (resole) {
       setTimeout(function () {
           console.log('promise中的定时器执行')
       })
       console.log('promise 执行')
       resole(1);
   }).then(function () {
       console.log('promise 回调执行');
       parent.setAttribute('data-spm','1.1.1.1');
   })
   console.log('最后一行');
</script>

再来展开我们的话题

前言

熟悉JavaScript的小伙伴肯定知道Event Loop(事件循环),由于JavaScript引擎是单线程的,同时只能处理一个任务,所有当JavaScript碰到某些耗时的任务,比如网络IO,为了不阻塞之后的代码执行,这时候就需要将该任务交由其他的线程去执行,并且监听一个事件回调,当异步的结果返回时,将该回调推入到异步的队列中去,而此时假如主线程已经执行完了后续的代码并且处于空闲状态,就会去依次执行异步队列中的代码。比如下面这样

setTimeout(function(){
	console.log('console start');
});
console.log('console end');

// console end
// console start

但是很多时候,JavaScript代码中有很多的“异步”操作,要是他们同时出现了,这时候要怎么判断他们执行的顺序呢。

正文

我们先拟定这么几个有关异步的API

1. setTimeout / setInterval
2. promise
3. Dom listener callback
4. MutationObserver

关于这个可能大家不那么熟悉,不过不影响这篇文章的论述,有兴趣可以移步这篇文章
https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver

开始测试

1. 我们先来测试setTimeout

    setTimeout(function () {
       console.log('定时器1执行')
   },0)
   setTimeout(function () {
       console.log('定时器2执行')
   },0)
   // 定时器1执行
   // 定时器2执行

两个定时器,延时都为0,先执行1,再执行2,好理解。

   setTimeout(function () {
       console.log('定时器1执行')
   },100)
   setTimeout(function () {
       console.log('定时器2执行')
   },50)
    // 定时器2执行
   // 定时器1执行

由于定时器2延时较少,所以更快进入异步队列,所以事件循环开始时,先执行定时器2的回调。

2. 测试Promise

    new Promise(function (resole) {
        console.log('promise 1 执行')
        resole(1);
    }).then(function () {
        console.log('promise 1回调执行');
        
    })
  	 new Promise(function (resole) {
        console.log('promise 2 执行')
        resole(2);
    }).then(function () {
        console.log('promise 2回调执行');
    })
 // promise 1 执行
 // promise 2 执行
 // promise 1回调执行
 // promise 2回调执行

可以看到,这里new Promise中的操作顺序执行完后,后续的回调也依次执行,没有特别的问题

    new Promise(function (resolve) {
       setTimeout(function(){
           resolve(1);
		   console.log('发布回调1');
       },500)
    }).then(function () {
        console.log('promise 1回调执行');
        
    })
   new Promise(function (resolve) {
          setTimeout(function(){
          resolve(2);
		  console.log('发布回调2');
       },100)
    }).then(function () {
        console.log('promise 2回调执行');  
    })
    // 发布回调2
    // promise 2回调执行
    // 发布回调1
    // promise 1回调执行

这个结果可能有一些匪夷所思,先放着,我们等会回头再来看这个问题。

3. 结合定时器和Promise

setTimeout(function(){
   console.log('定时器执行了');
})
new Promise(function(resolve){
    console.log('Promise!')
    resolve(1);
}).then(function(){
    console.log('Promise 回调执行');
});
// Promise!
// Promise 回调执行
// 定时器执行了

不知道小伙伴们会不会有疑问,也许觉得定时器应该在Promise的回调之前执行,这时候就可以扯出一些概念了。

一个猜想

主线程执行完后,假如产生了宏任务和微任务,那么他们的回调会被分别推到宏任务队列和微任务队列中去,等到下一次事件循环来到时,若存在微任务,则会先依次执行完所有的微任务,然后再依次执行所有的宏任务。接着进入下一次事件循环。
我们来对比测试3中的例子看看这个猜想。
由于Promise的回调优先定时器执行了,所以可以猜到Promise的回调属于微任务,而定时器属于宏任务。所以虽然定时器先进入异步队列,但是他是进入到宏任务队列,而Promise的回调则是进入到微任务队列,所以事件循环结束后,还是先执行了微任务队列中的Promise回调

再来看测试2中的代码,要注意的是,第一次主线程执行完毕后,只产生了俩个宏任务,即来个定时器的回调,因为还未被resolve,所以微任务还未产生。但是下一次事件循环来临时,定时器2这个宏任务先执行,要注意的是,在这个宏任务完成后,产生了一个新的微任务,即promise 2回调执行,他被马上执行了。随后再执行定时器1这个宏任务,最后执行定时器1产生的微任务。

继续猜想

假如一次事件循环途中,某个宏任务或者微任务产生了一个微任务,那么会立即执行完微任务,然后再依次执行其他的宏任务。执行完毕后,再进入到下一次事件循环。

宏任务和微任务怎么区分

这里我直接给出结论,大家有兴趣可以自行验证或者寻找资料。
setTimeOut ,setTimeInteral,事件回调 这类属于宏任务
promise ,mutation这类属于微任务

验证刚刚开头的例子

我们用以上的猜想来看看开头的例子,一步步分析。
首先给parent和child添加事件监听,然后我们直接执行 child.click(),注意,这里不是通过点击来触发,所以相当于是直接在主线程中执行这个点击方法。

1. 此时父节点在捕获阶段先响应,打印出“parent”
2. 然后子节点在冒泡阶段,打印出“child”
3. 定时器0被推到宏任务队列中,等待下一次事件循环,注意延时为100ms
4. 定时器1被推到宏任务队列中,等待下一次事件循环,注意延时为0ms
5. 定时器2被推到宏任务队列中,等待下一次事件循环,注意延时为0ms
6. 执行Promise中方法,注意这里又产生了一个定时器,推到宏任务队列中,等待下一次事件循环,注意延时为0ms。然后打印出“ promise 执行”。并且resolve后,产生了一个Promise回调的微任务。并且这个微任务中,又产生了一个mutation的微任务
7. 到主线程最后一行代码,打印出 “最后一行”
8. 进入下一次事件循环

所以刚刚开始的打印顺序是 parent,child,最后一行

新的事件循环开始
9. 在上述的第6步,产生了一个微任务,所以先执行,打印出“ promise 回调执行”,并且产生了新的微任务
10.  马上执行新的mutation的微任务,打印出 “mutate 属性改变”。
11. 现在就剩下4个定时器了,按照顺序依次执行,所以分别打印出 “定时器1执行”,“定时器2执行”,“promise中的定时器执行”,“定时器0执行”

最终结果

index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:15 parent
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:18 child
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:40 promise 执行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:46 最后一行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:43 promise 回调执行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:21 mutate 属性改变
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:31 定时器1执行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:34 定时器2执行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:38 promise中的定时器执行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:27 定时器0执行了

结论

注意到上述步骤6中,promise回调又产生了mutation的微任务,按照之前的猜想,他会被等到下一次事件循环,但是却在定时器1之前执行了,说明他是在这次事件循环被执行的。所以综上猜想,我们最后可以给出这样的结论

第一次主线程执行完后,当前如果有微任务,则先执行完所有的微任务。宏任务就推到宏任务队列中,等待下一次事件循环。然后进入下一次事件循环
然后,每执行完一个宏任务后,检测当前是否有微任务产生,有就立即执行所有微任务。有宏任务产生,则推到宏任务中,等待下一次事件循环。
接着依次重复执行完本次事件循环中的所有宏任务。然后进入下一次事件循环

上面的结论,略显繁琐,其实假如我们把第一次主线程的执行,即script下的所有代码,看成是第一次宏任务,那么核心结论可以变成这样

假如主线程执行栈不为空,那么执行完所有宏任务后,再执行所有微任务
否则,执行完一个宏任务后,会立即执行所有微任务

以上测试均在Chrome中进行。

后记

其实这些东西主要是要在脑子中形成一个印象,用来加深理解 事件循环 。而且,在Node的世界里,比如还会有其他的微任务,比如Process.nextTick。弄懂这些,对JavaScript的执行能有更好的把握。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章