本文主要對於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
複製程式碼
具體流程如圖所示
- 棧中程式碼的執行遵循先進後出原則
Javascript執行環境的執行機制
在Javascript中,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
主要流程:
- 所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
- 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
- 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
- 主執行緒不斷重複上面的第三步。 根據這個流程畫了一張簡單的草圖,不足之處,請大家指正,圖示如下:
瀏覽器的event loop
瀏覽器的event loop在html5的規範中明確定義。事件、使用者互動、指令碼、渲染、網路這些都是我們所熟悉的東西,他們都是由event loop協調的。觸發一個click事件,進行一次ajax請求,背後都有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');
複製程式碼
測試這段程式碼在不同的瀏覽器上顯示的順序
- 谷歌瀏覽器顯示的是這樣的
- 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);
});
複製程式碼
我們來看這段程式碼在瀏覽器中執行的具體結果:
程式碼執行過程: script裡的程式碼被列為一個macro-task,放入macro-task佇列。
迴圈1:
- 【macro-task佇列:script ;micro-task佇列:】
- 從macro-task佇列中取出script任務,推入棧中執行。
- promise2列為micro-task,setTimeout1列為macro-task,setTimeout2列為macro-task。
- 【task佇列:setTimeout1 setTimeout2;micro-task佇列:promise2】
- script任務執行完畢,執行micro-task checkpoint,取出micro-task佇列的promise2執行。
迴圈2:
- 【macro-task佇列:setTimeout1 setTimeout2;micro-task佇列:】
- 從macro-task佇列中取出setTimeout1,推入棧中執行,將promise1列為micro-task
- 【macro-task佇列:setTimeout2;micro-task佇列:promise1】
- 執行micro-task checkpoint,取出micro-task佇列的promise1執行。
迴圈3:
- 【macro-task佇列:setTimeout3;micro-task佇列:】
- 從macro-task佇列中取出setTimeout3,推入棧中執行。
- setTimeout3任務執行完畢,執行micro-task checkpoint。
- 【macro-task佇列:;micro-task佇列:promise3】
- 執行micro-task checkpoint,取出micro-task佇列的promise3執行。
event loop的處理過程
event loop的處理過程(Processing model) 在規範的Processing model定義了event loop的迴圈過程:
一個event loop只要存在,就會不斷執行下邊的步驟:
- 在tasks佇列中選擇最老的一個task,使用者代理可以選擇任何task佇列,如果沒有可選的任務,則跳到下邊的micro-tasks步驟。
- 將上邊選擇的task設定為正在執行的task。
- Run: 執行被選擇的task。
- 將event loop的currently running task變為null。
- 從task佇列裡移除前邊執行的task。
- micro-tasks: 執行micro-tasks任務檢查點。(也就是執行micro-tasks佇列裡的任務)
- 更新渲染(Update the rendering)...
- 如果這是一個worker event loop,但是沒有任務在task佇列中,並且WorkerGlobalScope物件的closing標識為true,則銷燬event loop,中止這些步驟,然後進行定義在Web workers章節的run a worker。
- 返回到第一步。
event loop會不斷迴圈上面的步驟,概括說來:
event loop會不斷迴圈的去取tasks佇列的中最老的一個任務推入棧中執行,並在當次迴圈裡依次執行並清空micro-task佇列裡的任務。