徹底搞懂瀏覽器Event-loop

劉小夕發表於2019-03-22

為什麼會寫這篇博文呢?

前段時間,和頭條的小夥伴聊天問頭條面試前端會問哪些問題,他稱如果是他面試的話,event-loop肯定是要問的。那天聊了蠻多,event-loop算是給我留下了很深的印象。原因很簡單,因為之前我從未深入瞭解過,如果是面試的時候,我遇到了這個問題,估計回答得肯定不如人意。

因此,最近我閱讀了一些相關的文章,並細細梳理了一番,輸出了本篇博文,希望能幫助大家搞懂瀏覽器的event-loop。後續會繼續補充node中的event-loop。

1. 預備知識

JavaScript的執行機制:

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。

(2)主執行緒之外,還存在"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步

概括即是: 呼叫棧中的同步任務都執行完畢,棧內被清空了,就代表主執行緒空閒了,這個時候就會去任務佇列中按照順序讀取一個任務放入到棧中執行。每次棧內被清空,都會去讀取任務佇列有沒有任務,有就讀取執行,一直迴圈讀取-執行的操作

一個事件迴圈中有一個或者是多個任務佇列

JavaScript中有兩種非同步任務:

  1. 巨集任務: script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering

  2. 微任務: process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;

2. 事件迴圈(event-loop)是什麼?

主執行緒從"任務佇列"中讀取執行事件,這個過程是迴圈不斷的,這個機制被稱為事件迴圈。此機制具體如下:主執行緒會不斷從任務佇列中按順序取任務執行,每執行完一個任務都會檢查microtask佇列是否為空(執行完一個任務的具體標誌是函式執行棧為空),如果不為空則會一次性執行完所有microtask。然後再進入下一個迴圈去任務佇列中取下一個任務執行。

詳細說明:

  1. 選擇當前要執行的巨集任務佇列,選擇一個最先進入任務佇列的巨集任務,如果沒有巨集任務可以選擇,則會跳轉至microtask的執行步驟。
  2. 將事件迴圈的當前執行巨集任務設定為已選擇的巨集任務。
  3. 執行巨集任務。
  4. 將事件迴圈的當前執行任務設定為null。
  5. 將執行完的巨集任務從巨集任務佇列中移除。
  6. microtasks步驟:進入microtask檢查點。
  7. 更新介面渲染。
  8. 返回第一步。

執行進入microtask檢查的的具體步驟如下:

  1. 設定進入microtask檢查點的標誌為true。
  2. 當事件迴圈的微任務佇列不為空時:選擇一個最先進入microtask佇列的microtask;設定事件迴圈的當前執行任務為已選擇的microtask;執行microtask;設定事件迴圈的當前執行任務為null;將執行結束的microtask從microtask佇列中移除。
  3. 對於相應事件迴圈的每個環境設定物件(environment settings object),通知它們哪些promise為rejected。
  4. 清理indexedDB的事務。
  5. 設定進入microtask檢查點的標誌為false。

需要注意的是:當前執行棧執行完畢時會立刻先處理所有微任務佇列中的事件, 然後再去巨集任務佇列中取出一個事件。同一次事件迴圈中, 微任務永遠在巨集任務之前執行。

圖示:

event-loop2

3. Event-loop 是如何工作的?

先看一個簡單的示例:

setTimeout(()=>{
    console.log("setTimeout1");
    Promise.resolve().then(data => {
        console.log(222);
    });
});
setTimeout(()=>{
    console.log("setTimeout2");
});
Promise.resolve().then(data=>{
    console.log(111);
});
複製程式碼

思考一下, 執行結果是什麼?

執行結果為:

111
setTimeout1
222
setTimeout2
複製程式碼

我們來看一下為什麼?

我們來詳細說明一下, JS引擎是如何執行這段程式碼的:

  1. 主執行緒上沒有需要執行的程式碼
  2. 接著遇到setTimeout 0,它的作用是在 0ms 後將回撥函式放到巨集任務佇列中(這個任務在下一次的事件迴圈中執行)。
  3. 接著遇到setTimeout 0,它的作用是在 0ms 後將回撥函式放到巨集任務佇列中(這個任務在再下一次的事件迴圈中執行)。
  4. 首先檢查微任務佇列, 即 microtask佇列,發現此佇列不為空,執行第一個promise的then回撥,輸出 '111'。
  5. 此時microtask佇列為空,進入下一個事件迴圈, 檢查巨集任務佇列,發現有 setTimeout的回撥函式,立即執行回撥函式輸出 'setTimeout1',檢查microtask 佇列,發現佇列不為空,執行promise的then回撥,輸出'222',microtask佇列為空,進入下一個事件迴圈。
  6. 檢查巨集任務佇列,發現有 setTimeout的回撥函式, 立即執行回撥函式輸出'setTimeout2'。

再思考一下下面程式碼的執行順序:

console.log('script start');

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

setTimeout(function () {
    console.log('setTimeout---200');
    setTimeout(function () {
        console.log('inner-setTimeout---0');
    });
    Promise.resolve().then(function () {
        console.log('promise5');
    });
}, 200);

Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});
Promise.resolve().then(function () {
    console.log('promise3');
});
console.log('script end');
複製程式碼

思考一下, 執行結果是什麼?

執行結果為:

script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
複製程式碼

那麼為什麼?

我們來詳細說明一下, JS引擎是如何執行這段程式碼的:

  1. 首先順序執行完主程式上的同步任務,第一句和最後一句的console.log
  2. 接著遇到setTimeout 0,它的作用是在 0ms 後將回撥函式放到巨集任務佇列中(這個任務在下一次的事件迴圈中執行)。
  3. 接著遇到setTimeout 200,它的作用是在 200ms 後將回撥函式放到巨集任務佇列中(這個任務在再下一次的事件迴圈中執行)。
  4. 同步任務執行完之後,首先檢查微任務佇列, 即 microtask佇列,發現此佇列不為空,執行第一個promise的then回撥,輸出 'promise1',然後執行第二個promise的then回撥,輸出'promise3',由於第一個promise的.then()的返回依然是promise,所以第二個.then()會放到microtask佇列繼續執行,輸出 'promise2';
  5. 此時microtask佇列為空,進入下一個事件迴圈, 檢查巨集任務佇列,發現有 setTimeout的回撥函式,立即執行回撥函式輸出 'setTimeout---0',檢查microtask 佇列,佇列為空,進入下一次事件迴圈.
  6. 檢查巨集任務佇列,發現有 setTimeout的回撥函式, 立即執行回撥函式輸出'setTimeout---200'.
  7. 接著遇到setTimeout 0,它的作用是在 0ms 後將回撥函式放到巨集任務佇列中,檢查微任務佇列,即 microtask 佇列,發現此佇列不為空,執行promise的then回撥,輸出'promise5'。
  8. 此時microtask佇列為空,進入下一個事件迴圈,檢查巨集任務佇列,發現有 setTimeout 的回撥函式,立即執行回撥函式輸出,輸出'inner-setTimeout---0'。程式碼執行結束.

4. 為什麼會需要event-loop?

因為 JavaScript 是單執行緒的。單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。為了協調事件(event),使用者互動(user interaction),指令碼(script),渲染(rendering),網路(networking)等,使用者代理(user agent)必須使用事件迴圈(event loops)。

最後有一點需要注意的是:本文介紹的是瀏覽器的Event-loop,因此在測試驗證時,一定要使用瀏覽器環境進行測試驗證,如果使用了node環境,那麼結果不一定是如上所說。

5. 參考文章:

  1. segmentfault.com/a/119000001…
  2. segmentfault.com/a/119000001…
  3. segmentfault.com/a/119000001…
  4. www.ruanyifeng.com/blog/2014/1…

最後,如果您覺得本篇博文給了您一點幫助或者啟發,請幫忙點個Star吧~ github.com/YvetteLau/B…

歡迎關注小姐姐的微信公眾號:前端宇宙。

徹底搞懂瀏覽器Event-loop

相關文章