徹底理解 JS Event Loop(瀏覽器環境)

93發表於2018-03-10

最近羅列了一些軟體開發基礎知識點,計劃逐一的、徹底的理解每一個知識點,併為每個知識點寫一篇詳細的,圖文並茂的文章。這篇是關於瀏覽器環境下 JS 的 Event Loop 機制(如有錯誤,歡迎指出)。

瀏覽器執行緒

我們常說 JS 是單執行緒語言,但是別忘了常見的瀏覽器核心可都是多執行緒的,多個執行緒間會進行不斷通訊,通常會有如下幾個執行緒:

  • GUI 渲染程式
  • JS 引擎執行緒
  • 定時器執行緒
  • 事件觸發執行緒
  • 非同步 HTTP 請求執行緒

Microtask 與 Macrotask

在大多數解釋 JS Event Loop 的文章中,鮮有談及 Miscrotask 和 Macrotask 這兩個概念,但這兩個概念卻是非常的重要,我在翻閱 Zone.js Primer 時,裡面就經常會提及這兩個概念,當時也是看的雲裡霧裡的,這也是我寫這篇文章的原因之一。

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

console.log('start');

Promise.resolve().then(function () {
    console.log('promise1');
    Promise.resolve().then(function () {
        console.log('promise2');
    });
    setTimeout(function () {
        Promise.resolve().then(function () {
            console.log('promise3');
        });
        console.log('timeout2')
    }, 0);
});

console.log('done');
複製程式碼

以上程式碼最後會輸出什麼呢?如果你能很快的回答出來,你大概就已經掌握了 Event Loop 的實際運用了,如果回答不出,那可能還得接著往下看。

問題:是先執行 then( ) 中的回撥函式呢,還是 setTimeout( ) 中的回撥函式呢?

答案:先執行前者。因為 Promise.prototype.then( ) 是 Microtask ,而 setTimeout( ) 是 Macrotask 。至於為什麼先執行 Miscrotask ?繼續往後看~

在 JS 執行緒中程式的每一個呼叫都被看成是一個任務(task) ,所有的任務被分成許多型別且存放在對應型別的佇列中,為了方便理解,我把這些任務佇列分成三類:

  • Micro-task queue: 存放 microtask 的回撥函式。

  • Macro-task queue: 存放 macrotask 的回撥函式 。

  • Other-task queue: 這是一個我個人抽象出來佇列,實際並不存在,假設該佇列用來存放除了 microtask 和 macrotask 外的所有任務。

Microtask 和 Macrotask 的區別就是執行順序上的區別。簡單的說,JS 執行緒會先處理 other-task queue 上的任務,處理完了之後,再去處理 micro-task queue 上的任務,最後才處理 macro-task queue 上的任務。至於 JS 執行緒具體的執行細節,後面會詳細的進行描述。

以下是常見的 Microtask 和 Macrotask:

  • Microtask :Promise.prototype.then( )、MutationObserver.prototype.observe( ) 等 。

  • Macrotask :setTimeout( )、setImmediate( )、XMLHttpRequest.prototype.onload( ) 等。

JS 執行緒 Event Loop 的實現

Event Loop 模型圖

如上,根據個人的理解,我畫了一個瀏覽器環境下 JS 實現 Event Loop 大致模型圖,具體含義如下:

1 獲取執行的任務,執行步驟 1.1

1.1 判斷 other-task queue 中是否有任務,如果有,獲取最早的任務然後執行步驟 2 ,否則執行步驟 1.2 。

1.2 判斷 micro-task queue 中是否有任務,如果有,獲取最早的任務然後執行步驟 2 ,否則執行步驟 1.3 。

1.3 判斷 macro-task queue 中是否有任務,如果有,獲取最早的任務然後任何執行步驟 2 ,否則執行步驟 3 。

2 將取到的任務放到 call stack 並執行,執行完之後再執行步驟 1 (值得注意的是,在執行的過程中,是會不斷的更新所有的 task queue ,因為 call stack 中正在執行的任務內部也可能存在普通任務、microtask 和 macrotask ,執行任務的過程可以理解為一個遞迴過程,如果無限遞迴,call stack 上待執行的任務就會不斷累積而溢位,這也就是常見的 Maximum call stack size exceeded 錯誤)。

3 執行緒會處理其他工作,例如:不斷同步「事件觸發執行緒」的狀態,一旦有事件觸發,即檢視觸發事件「target」有沒有對應事件的監聽器任務,如果有,則選中該任務並執行步驟 2 。需要注意的是,並不是只有執行了步驟 1.3 後才會執行當前步驟,JS 執行緒肯定還會在的某個時候去同步其他執行緒的狀態的。

接下來,如果仔細想,可能會產生一個疑問:JS 程式是如何更新 micro-task queue 和 macro-task queue 這兩個佇列的呢 ?

根據我的理解,micro-task queue 和 other-task queue 都是“同步”更新的,而 macro-task queue 是“非同步”更新。以下是 macro-task queue 更新的具體流程(以 setTimeout 為例):

  1. JS 執行緒判斷某個 macrotask 是一個定時器,將這個定時器同步給定時器執行緒。
  2. 定時器執行緒啟動從 JS 執行緒收到的定時器。
  3. JS 執行緒在某個時候(可能是執行上述步驟 1 的時候)會通過定時器和 http 請求等一些執行緒來更新 macro-task queue ,即如果以上的定時結束了,JS 執行緒就可以將對應定時器的回撥函式存放到 macro-task queue 中。

如何理解 JS 中的非同步

目前普遍對非同步的解釋可能是:執行呼叫,如果立即得到結果就是同步呼叫,否則為非同步呼叫。

在 JS 環境中,我個人其實是不同意這個解釋的。

首先,根據以上的解釋,setTimeout( )、Promise.prototype.then( ) 、http 請求和各類瀏覽器事件,這些都被認為是非同步的。但我卻不這麼認為,我認為瀏覽器事件不是非同步的。以下程式碼便是理由:

// html: <button id="btn">click</button>

// js
var btn = document.getElementById('btn');

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

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

btn.addEventListener('click', function () {
    console.log('click');
});

btn.click();

console.log('done');
複製程式碼

如果瀏覽器事件是非同步的,不管後續會列印出什麼,第一個列印的必然是 done ,而實際的列印結果為:click done promise timeout

也就是說,JS 認為瀏覽器事件並非非同步。

由此,我個人對非同步的解釋是:在滿足呼叫所需的外在條件的情況下,執行呼叫,立即獲得結果的就為同步呼叫,否則為非同步呼叫。

根據這個理解,當我們發起的一個 http 請求時,假設伺服器以光速返回請求結果,XMLHttpRequest 物件的 onload 方法會立即執行嗎?,顯然不會,所以 http 請求為非同步呼叫。這也是為什麼我在以上分析 Event Loop 中的任務佇列時並沒有將 event-task queue 拎出來的原因。因此,對於非同步呼叫的判斷可以是這樣:如果某個呼叫屬於 microtask 或是 macrotask 中的其中一個,那麼這個呼叫就是非同步呼叫。

題外話

有人可能會注意到,這篇文章經常出現「我認為」和「我理解」,這並非是我對自己不自信,而是我想表達一個看法:在翻閱別人的技術文章的時候,務必保持獨立思考的能力,就算文章的作者是業界有名的大牛,也不能沒緣由的「深信不疑」,對對應的技術點務必在自個腦中裡建立一個可以自圓其說的模型。至於我為什麼會表達這個看法,是因為我找翻閱大量的過程中,發現大多數關於 JS Event Loop 的文章或多或少都有一些粗糙或是錯誤,如果我只看其中的某一篇,我很大的概率會有建立一個錯誤的 Event Loop 模型。當然,就我當前的理解,還是可能會有些許錯誤。Anyway ,還是那句話:保持獨立思考,與各位共勉。

Done.?

參考連結

相關文章