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的執行能有更好的把握。

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