js異步從入門到放棄(三)- 異步任務隊列(task queues)

前言

本文是對於異步系列第一篇裏提到的evenloop模型中,所提到的任務隊列(task queues)的展開分析html

正文

說明:如下代碼均使用chrome瀏覽器運行 關於瀏覽器表現的差別在最後作補充。web

引子-奇怪的執行順序

先看一個典型的例子:chrome

console.log('script start')
// 第一個異步任務
setTimeout(()=>{
    console.log('setTimeout')
},0)

// 第二個異步任務
Promise.resolve().then(()=>{
    console.log('promise1')
}).then(()=>{ 
  console.log('promise2');
})
console.log('script end')
// 實際輸出結果: 
// script start
// script end
// promise1
// promise2
// setTimeout

根據以前說過的evenloop模型,首先輸出script startscript end沒有什麼問題;
可是接下來卻發現:
先執行了Promise指定的callback而不是setTimeoutcallback-- Why?api

兩種任務隊列(microtask queue&macrotask queue)

在以前討論evenloop模型時,粗略提到了任務隊列有2種類型:microtask queuemacrotask queue,他們的區別在於:promise

  • macrotask的執行:是在evenloop的每次循環過程,取出macrotask queue中可執行的第一個(注意不必定是第一個,由於咱們說過例如setTimeout能夠指定任務被執行的最少延遲時間,當前macrotask queue的首位保存的任務可能尚未到執行時間,因此queue只是表明callback插入的順序,不表明執行時也要按照這個順序)。
  • microtask的執行:在evenloop的每次循環過程以後,若是當前的執行棧(call stack)爲空,那麼執行microtask queue中全部可執行的任務

(某些文獻內容中 直接把macrotask稱爲task,或者某些中文文章中把它們翻譯成"微任務"和"宏任務",含義都是類似的:macrotask或者task表明相對單獨佔據evenloop過程一次循環的任務,而microtask有可能在一次循環中執行多個)瀏覽器

如今回頭來解析前面的例子:app

  1. 第一次執行主函數,輸出script start
  2. 遇到setTimeout,將對應的callback插入macrotask queue
  3. 遇到promise,將對應的callback插入microtask queue
  4. 輸出script end,主函數運行結束,執行棧清空,此時開始檢查microtask queue,發現裏面有可運行的任務,所以按順序輸出promise1promise2
  5. microtask queue執行完,開始新一輪循環,從macrotask queue取出setTimeout任務並執行,輸出setTimeout
  6. 結束,呈現上面的輸出結果。

常見異步操做對應的回調函數任務類型以下:dom

  • macrotask: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
  • microtask: process.nextTick, Promises, Object.observe, MutationObserver

大概能夠這樣區分:和html交互密切相關的異步操做,通常是macrotasks;由emcascript的相關接口返回的異步操做,通常是microtaskswebapp

如何判斷執行順序

接下來看一個更復雜的例子,幫助理解不一樣異步任務的執行順序異步

<style>
    .outer {
        padding: 30px;
        background-color: aqua;

    }

    .inner {
        height: 100px;
        background-color: brown;
    }
</style>

<body>
    <div class="outer">outer
         <div class="inner">inner</div> 
    </div>
</body>
<script>
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');

    // Let's listen for attribute changes on the
    // outer element
    new MutationObserver(function () {
        console.log('mutate');
    }).observe(outer, {
        attributes: true
    });

    // Here's a click listener…
    function onClick() {
        console.log('click');

        setTimeout(function () {
            console.log('timeout');
        }, 0);

        Promise.resolve().then(function () {
            console.log('promise');
        });

        outer.setAttribute('data-random', Math.random());
    }

    // …which we'll attach to both elements
    inner.addEventListener('click', onClick);
    outer.addEventListener('click', onClick);

運行以上代碼,能夠在瀏覽器看到兩個嵌套的div(如圖):
demo

點擊inner部分,打開chrome的調試器,能夠看到console打出的結果是:

  1. click
  2. promise
  3. mutate
  4. click
  5. promise
  6. mutate
  7. timeout
  8. timeout

接下來分析運行過程 (建議打開chrome單步調試,進行觀察分析):

  1. 點擊inner,觸發對應的onClick事件,此時inner對應的onClick函數進入執行棧;
  2. 運行console.log('click'),輸出(1)click
  3. 運行setTimeout,macrotask queue添加對應的console函數
  4. 運行Promise,此時microtask queue添加對應的console函數
  5. 運行outer.setAttribute,觸發MutationObserver,microtask queue添加對應的console函數(前面註明了MutationObserver建立的回調任務類型是microtask)
  6. 當前函數執行完畢,因爲執行棧清空,此時開始調度microtask queue,所以依次輸出(2)promise(3)mutate,此時當前執行棧call stackmicrotask queue均爲空,可是macrotask queue裏依然存儲着兩個東西--inner的Click觸發的任務,以及先前setTimeout的回調函數。
  7. inner的onclick函數雖然執行完畢,可是因爲事件冒泡,緊接着要觸發outeronClick的執行函數,所以setTimeout的回調暫時還沒法執行。
  8. outeronClick函數執行過程,重複前面的2-5步驟,所以輸出(4)click (5)promise (6)mutate
  9. 此時執行棧call stackmicrotask queue均爲空,macrotask queue存儲着兩個setTimeout的回調函數。,根據evenloop模型,開始分別執行這兩個task,因而輸出了兩個(7)和(8)timeout
  10. 結束。

再次建議在調試器查看上面的步驟,尤爲要注意觀察call stackmicrotask queue macrotask queue的變化,會更加直觀

在充分理解上面例子的基礎上,咱們把點擊inner部分的這個操做,改爲直接在js代碼的末尾加上innner.click(),請問結果是否一致呢?

先說最終結果:

  1. click
  2. click
  3. promise
  4. mutate
  5. promise
  6. timeout
  7. timeout

與前一次的結果徹底不一樣!
接下來再次進入調試分析:

  1. 因爲是直接執行inner.click(),此次進入inner綁定的onclick函數時,與前面是有所不一樣的:

經過chrome調試器能夠看到,此時的call stack有兩層--除了onClick函數以外,還有一層匿名函數,這層函數其實就是最外層的script,至關於window.onload綁定的處理函數。

這是很關鍵的一點!!!就是這一個區別,致使了整個執行結果的差別。
由於前面的例子的執行順序是:

  1. 頁面加載後先運行了整個匿名函數
  2. 函數出棧
  3. 點擊時觸發inner的onclcik
  4. 此時onClick對應的函數進棧。

兩次執行到onclick時的callstck區別如圖:

第一次,經過點擊inner觸發click
圖片描述

第二次,經過代碼直接觸發click
圖片描述

接下來分析本次的輸出順序:

  1. 重複前面例子中,步驟2-5,輸出一個(1)click
  2. inneronClick函數執行完畢,可是此次執行棧並未清空,由於當前匿名函數還在執行棧裏,所以沒法開始調度microtask queue!!!(前面說過,microtask queue的調度必須在當前執行棧爲空的狀況下),所以,這時候會先進入冒泡事件觸發的onClick
  3. 相似的,輸出(2)clcik以後,promise的回調函數進入microtask queue
  4. 運行outer.setAttribute,觸發MutationObserver,可是此時microtask queue沒法再次添加對應的回調函數了,由於已經有一個存在的監聽函數在pengding
  5. 兩個onclick執行完畢,執行棧清空,接下來開始調度microtask queue,輸出(3)promise (4)mutate (5)promise
  6. 此時當前執行棧call stackmicrotask queue均爲空,macrotask queue存儲着兩個setTimeout的回調函數。根據evenloop模型,開始分別執行這兩個task,因而輸出了兩個(6)和(7)timeout
  7. 結束

這兩個例子的對比,着重說明了一點:
--microtask queue存儲的任務,必需要在當前函數執行棧爲空時纔會開始調度。
完整內容可參見html標準中的8.1.4部分

結論

  1. macrotask會按順序執行,而且有可能被中途插入瀏覽器render,例如上面的冒泡事件
  2. microtask的執行有兩個條件:

    1. 在每一個macrotask結束以後
    2. 當前call stack爲空

ps:瀏覽器差別

上述代碼在chrome的瀏覽器下測試結果,可能和在某些版本的firefox和ie瀏覽器下不一致,在某些瀏覽器中可能會把promise的回調函數當作mascrotask,可是:

廣泛的共識把 Promise當作是 miscrotask,而且有比較充分的理由:若是把promose當作是task(即mascrotask)將會致使一些性能問題--由於task的調度是能夠被其餘task相關的任務如 Render打斷,還會由於與其餘任務源的交互致使不肯定性。

參考文獻

  1. Tasks, microtasks, queues and schedules
  2. HTML Living Standard

若是以爲寫得很差/有錯誤/表述不明確,都歡迎指出
若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處。若是有問題也歡迎私信交流,主頁有郵箱地址
若是以爲做者很辛苦,也歡迎打賞~

相關文章
相關標籤/搜索