完全搞懂瀏覽器Event-loop

爲何會寫這篇博文呢?javascript

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

所以,最近我閱讀了一些相關的文章,並細細梳理了一番,輸出了本篇博文,但願能幫助你們搞懂瀏覽器的event-loop。後續會繼續補充node中的event-loop。前端

更多文章可戳: github.com/YvetteLau/B…java

1. 預備知識

JavaScript的運行機制:node

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。git

(2)主線程以外,還存在"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。github

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。面試

(4)主線程不斷重複上面的第三步shell

歸納便是: 調用棧中的同步任務都執行完畢,棧內被清空了,就表明主線程空閒了,這個時候就會去任務隊列中按照順序讀取一個任務放入到棧中執行。每次棧內被清空,都會去讀取任務隊列有沒有任務,有就讀取執行,一直循環讀取-執行的操做segmentfault

一個事件循環中有一個或者是多個任務隊列

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…

推薦關注本人公衆號

相關文章
相關標籤/搜索