一文梳理JavaScript 事件迴圈(Event Loop)

川南煙雨發表於2020-06-18

事件迴圈(Event Loop),是每個JS開發者都會接觸到的概念,但是剛接觸時可能會存在各種疑惑。

眾所周知,JS是單執行緒的,即同一時間只能執行一個任務。一般情況下這不會引發問題,但是如果我們有一個耗時較多的任務,我們必須等該任務執行完畢才能進入下一個任務,然而等待的這段時間常常讓我們無法忍受,因為我們這段時間什麼都不能做,包括頁面也是鎖死狀態。

好在,時代在進步,瀏覽器向我們提供了JS引擎不具備的特性:Web API。Web API包括DOM API、定時器、HTTP請求等特性,可以幫助我們實現非同步、非阻塞的行為。我們可以通過非同步執行任務的方法來解決單執行緒的弊端,事件迴圈為此而生

提問QAQ:為什麼JavaScript是單執行緒的?

多個執行緒表示您可以同時獨立執行程式的多個部分。確定一種語言是單執行緒還是多執行緒的最簡單方法是看它擁有有多少個呼叫堆疊。JS 只有一個,所以它是單執行緒語言。

將JS設計為單執行緒是由其用途執行環境等因素決定的,作為瀏覽器指令碼語言,JS的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。同時,單執行緒執行效率高。

1. Event Loop舊印象

大家熟悉的關於事件迴圈的機制說法大概是:主程式執行完了之後,每次從任務佇列裡取一個任務執行。如圖所示,所有的任務分為同步任務和非同步任務,同步任務直接進入任務佇列-->主程式執行;非同步任務則會掛起,等待其有返回值時進入任務佇列從而被主程式執行。非同步任務會通過任務佇列的機制(先進先出的機制)來進行協調。具體如圖所示:

同步和非同步任務分別進入不同的執行環境,同步的進入主執行緒,即主執行棧,非同步的進入任務佇列。主執行緒內的任務執行完畢為空,會去任務佇列讀取對應的任務,推入主執行緒執行。 上述過程的不斷重複就是我們所熟悉的Event Loop (事件迴圈)。但是promise出現之後,這個說法就不太準確了

2. Event Loop 後印象

2.1 理論

這裡首先用一張圖展示JavaScript的事件迴圈:

直接看這張圖,可能黑人問號已經出現在同學的腦海。。。

這裡將task分為兩大類,分別是macroTask(巨集任務)和microTask(微任務).一次事件迴圈:先執行macroTask佇列中的一個,然後執行microTask佇列中的所有任務。接著開始下一次迴圈(只是針對macroTask和microTask,一次完整的事件迴圈會比這個複雜的多)。

那什麼是macroTask?什麼是microTask呢?

JavaScript引擎把我們的所有任務分門別類,一部分歸為macroTask,另外一部分歸為microTack,下面是類別劃分:

macroTask:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

microTask:

  • process.nextTick
  • Promise
  • Object.observe
  • MutationObserver

我們所熟悉的定時器就屬於macroTask,僅僅瞭解macroTask的機制還是不夠的。為直觀感受兩種佇列的區別,下面上程式碼進行實踐感知。

2.2 實踐

以setTimeout、process.nextTick、promise為例直觀感受下兩種任務佇列的執行方式。

console.log('main1');

process.nextTick(function() {
    console.log('process.nextTick1');
});

setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);

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

console.log('main2');

彆著急看答案,先以上面的理論自己想想,執行結果會是啥?

最終結果是這樣的:

main1
promise
main2
process.nextTick1
promise then

// 第二次事件迴圈
setTimeout
process.nextTick2

process.nextTick 和 promise then在 setTimeout 前面輸出,已經證明了macroTask和microTask的執行順序。但是有一點必須要指出的是。上面的圖容易給人一個錯覺,就是主程式的程式碼執行之後,會先呼叫macroTask,再呼叫microTask,這樣在第一個迴圈裡一定是macroTask在前,microTask在後。

但是最終的實踐證明:在第一個迴圈裡,process.nextTick1和promise then這兩個microTask是在setTimeout這個macroTask裡之前輸出的,這是因為Promises/A+規範規定主程式的程式碼也屬於macroTask。

主程式這個macroTask(也就是main1、promise和main2)執行完了,自然會去執行process.nextTick1和promise then這兩個microTask。這是第一個迴圈。之後的setTimeout和process.nextTick2屬於第二個迴圈

別看上面那段程式碼好像特別繞,把原理弄清楚了,都一樣 ~

requestAnimationFrame、Object.observe(已廢棄) 和 MutationObserver這三個任務的執行機制大家可以從上面看到,不同的只是具體用法不同。重點說下UI rendering。在HTML規範:event-loop-processing-model裡敘述了一次事件迴圈的處理過程,在處理了macroTask和microTask之後,會進行一次Update the rendering,其中細節比較多,總的來說會進行一次UI的重新渲染。

3. 小結

總而言之,記住一次事件迴圈:先執行macroTask佇列中的一個,然後執行microTask佇列中的所有任務。接著開始下一次迴圈。

參考文獻:

相關文章