瀏覽器的event loop,一起來了解下吧

tenor發表於2018-05-28

本文主要對於javascript執行環境---瀏覽器的event loop機制的一些理解,不足之處,請多多指正。

Javascript單執行緒

JavaScript語言單執行緒的,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript只能是單執行緒呢?

JavaScript的單執行緒,與它的用途有關。作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器就不知道該以哪個執行緒為主。

雖然在Html5新規範中定義,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,並且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

首先,我們來看看Javascript程式碼執行過程中都會有哪些東西?

程式碼執行棧(JavaScript execution context stack)

在程式碼執行過程中,主執行緒有一個棧,每一個函式執行的時候,都會生成新的execution context(執行上下文),執行上下文會包含一些當前函式的引數、區域性變數之類的資訊,它會被推入棧中, running execution context(正在執行的上下文)始終處於棧的頂部。當函式執行完後,它的執行上下文會從棧彈出。

現在我們來看看這段程式碼的執行順序:

function foo2() {
    console.log('foo2');
}
function foo1() {
    console.log('foo1');
    foo2();
}
foo1();
複製程式碼

程式碼結果是:

// foo1
// foo2
複製程式碼

具體流程如圖所示

  • 棧中程式碼的執行遵循先進後出原則

瀏覽器的event loop,一起來了解下吧

Javascript執行環境的執行機制

在Javascript中,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

主要流程:

  1. 所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
  2. 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
  4. 主執行緒不斷重複上面的第三步。 根據這個流程畫了一張簡單的草圖,不足之處,請大家指正,圖示如下:

瀏覽器的event loop,一起來了解下吧

瀏覽器的event loop

瀏覽器的event loop在html5的規範中明確定義。事件、使用者互動、指令碼、渲染、網路這些都是我們所熟悉的東西,他們都是由event loop協調的。觸發一個click事件,進行一次ajax請求,背後都有event loop在運作。

什麼是event loop

主執行緒不斷的從任務佇列中獲取事件來執行,這個過程是不斷的迴圈的,這樣的一種執行機制也就被稱之為Event Loop(事件迴圈)

瀏覽器的event loop,一起來了解下吧

  • 主執行緒在執行時,會生成堆和棧,裡面儲存的是呼叫的程式碼
  • 棧中呼叫的程式碼會呼叫各種外部的API,這些程式碼執行會在任務佇列中新增各種事件
  • 只要棧中的程式碼執行完成,主執行緒就會去讀取任務佇列,依次執行任務佇列中事件對應的回撥函式
  • 棧中的同步程式碼總會先於任務佇列中的非同步程式碼執行

現在我們來看這段程式碼在瀏覽器中是如何執行的:

console.log('1');

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

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

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

測試這段程式碼在不同的瀏覽器上顯示的順序

  • 谷歌瀏覽器顯示的是這樣的

瀏覽器的event loop,一起來了解下吧

  • safari 9.1.2瀏覽器中顯示的卻是promise1 promise2會在setTimeout的後邊

為什麼會出現不同瀏覽器解析這段程式碼輸出不同的順序? 在瞭解完macro-task和micro-task之後就明白了

macro-task

一個event loop有一個或者多個macro-task佇列。 當使用者代理安排一個任務,必須將該任務增加到相應的event loop的一個macri-tsak佇列中。

每一個macro-task都來源於指定的任務源,比如可以為滑鼠、鍵盤事件提供一個macro-task佇列,其他事件又是一個單獨的佇列。可以為滑鼠、鍵盤事件分配更多的時間,保證互動的流暢。

那麼有哪些任務源是macro-task任務源呢?

  • DOM操作任務源:此任務源被用來相應dom操作,例如一個元素以非阻塞的方式插入文件。
  • 網路任務源:網路任務源被用來響應網路活動。

macro-task任務源包含的範圍比較廣泛,基本上我們對DOM元素進行事件繫結並且繫結的各個事件都是macro-task任務源,需要注意的是setTimeout、setInterval、setImmediate也是macro-task任務源。所以macro-task任務源可以總結為

  • setTimeout
  • setInterval
  • setImmediate
  • I/O

micro-task

除了macro-task還有一個micro-task,這一個概念是ES6提出Promise以後出現的。這個micro-task queue只有一個。並且會在且一定會在每一個macro-task後執行,且執行是按順序的。加入到micro-task的事件型別有Promise.resolve().then(), process.nextTick()值得注意的是,event loop一定會在執行完micrtask以後才會尋找新的可執行的macrotask佇列。

常見的產生micro-task事件的方法:

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

在Promises/A+規範的Notes 3.1中提及了promise的then方法可以採用"巨集任務(macro-task)"機制或者"微任務(micro-task)"機制來實現。所以開頭提及的promise在不同瀏覽器的差異正源於此,有的瀏覽器將then放入了macro-task佇列,有的放入了micro-task佇列。

那讓我們再來看一段程式碼:

setTimeout(function setTimeout2() {
    console.log('setTimeout1');
}, 0);
setTimeout(function setTimeout2() {
    console.log('setTimeout2');
    Promise.resolve().then(function promise1() {
        console.log('promise1');
    });
}, 0);
Promise.resolve().then(function promise2() {
    console.log('promise2');
    setTimeout(function setTimeout1() {
        console.log('setTimeout3');
        Promise.resolve().then(function promise1() {
            console.log('promise3');
        });
    }, 0);
});
複製程式碼

我們來看這段程式碼在瀏覽器中執行的具體結果:

瀏覽器的event loop,一起來了解下吧

程式碼執行過程: script裡的程式碼被列為一個macro-task,放入macro-task佇列。

迴圈1:

  1. 【macro-task佇列:script ;micro-task佇列:】
  2. 從macro-task佇列中取出script任務,推入棧中執行。
  3. promise2列為micro-task,setTimeout1列為macro-task,setTimeout2列為macro-task。
  4. 【task佇列:setTimeout1 setTimeout2;micro-task佇列:promise2】
  5. script任務執行完畢,執行micro-task checkpoint,取出micro-task佇列的promise2執行。

迴圈2:

  1. 【macro-task佇列:setTimeout1 setTimeout2;micro-task佇列:】
  2. 從macro-task佇列中取出setTimeout1,推入棧中執行,將promise1列為micro-task
  3. 【macro-task佇列:setTimeout2;micro-task佇列:promise1】
  4. 執行micro-task checkpoint,取出micro-task佇列的promise1執行。

迴圈3:

  1. 【macro-task佇列:setTimeout3;micro-task佇列:】
  2. 從macro-task佇列中取出setTimeout3,推入棧中執行。
  3. setTimeout3任務執行完畢,執行micro-task checkpoint。
  4. 【macro-task佇列:;micro-task佇列:promise3】
  5. 執行micro-task checkpoint,取出micro-task佇列的promise3執行。

event loop的處理過程

event loop的處理過程(Processing model) 在規範的Processing model定義了event loop的迴圈過程:

一個event loop只要存在,就會不斷執行下邊的步驟:

  1. 在tasks佇列中選擇最老的一個task,使用者代理可以選擇任何task佇列,如果沒有可選的任務,則跳到下邊的micro-tasks步驟。
  2. 將上邊選擇的task設定為正在執行的task。
  3. Run: 執行被選擇的task。
  4. 將event loop的currently running task變為null。
  5. 從task佇列裡移除前邊執行的task。
  6. micro-tasks: 執行micro-tasks任務檢查點。(也就是執行micro-tasks佇列裡的任務)
  7. 更新渲染(Update the rendering)...
  8. 如果這是一個worker event loop,但是沒有任務在task佇列中,並且WorkerGlobalScope物件的closing標識為true,則銷燬event loop,中止這些步驟,然後進行定義在Web workers章節的run a worker。
  9. 返回到第一步。

event loop會不斷迴圈上面的步驟,概括說來:

event loop會不斷迴圈的去取tasks佇列的中最老的一個任務推入棧中執行,並在當次迴圈裡依次執行並清空micro-task佇列裡的任務。

文中的圖示是根據網路文件整理而成,不足之處或者有錯誤的地方,請指出,謝謝

相關文章