FE.ES-理解Event Loop

JavaScript引擎又稱爲JavaScript解釋器,是JavaScript解釋爲機器碼的工具,分別運行在瀏覽器和Node中。而根據上下文的不一樣,Event loop也有不一樣的實現:其中Node使用了libuv庫來實現Event loop; 而在瀏覽器中,html規範定義了Event loop,具體的實現則交給不一樣的廠商去完成。javascript

瀏覽器中的Event Loops

根據2017年新版的HTML規範HTML Standard,瀏覽器包含2類事件循環:browsing contexts 和 web workers。 html

browsing contexts中有一個或多個Task Queue,即MacroTask Queue,僅有一個Job Queue,即MicroTask Queue。html5

  • macrotask queue(宏任務,不妨稱爲Ajava

    • setTimeout
    • setInterval
    • setImmediate(node獨有
    • requestAnimationFrame
    • I/O
    • UI rendering
  • microtask queue(微任務,不妨稱爲Inode

    • process.nextTick(node獨有
    • Promises
    • Object.observe(廢棄)
    • MutationObserver

這兩個任務隊列執行順序:git

  • 取1個A中的task,執行之。
  • 把全部I順序執行完,再取A中的下一個任務。

clipboard.png

爲何promise.then的回調比setTimeout先執行
代碼開始執行時,全部這些代碼在A中,造成一個執行棧(execution context stack),取出來執行之。
遇到setTimeout,則加到A中,遇到promise.then,則加到I中。
等整個執行棧執行完,取I中的任務。github

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
// 1
// 2
// 3
// 5
// 4
//瀏覽器渲染步驟:Structure(構建 DOM) ->Layout(排版)->Paint(繪製) 
//新的異步任務將在下一次被執行,所以就不會存在阻塞。
button.addEventListener('click', () => {
  setTimeout(fn, 0)
})

V8源碼
https://github.com/v8/v8/blob...
https://github.com/v8/v8/blob...web


NodeJS中的Event Loop

而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。c#

clipboard.png

node新加了一個微任務process.nextTick和一個宏任務setImmediate.segmentfault

process.nextTick

在當前"執行棧"的尾部(下一次Event Loop以前)觸發回調函數。也就是說,它指定的任務老是發生在全部異步任務以前。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

setImmediate

setImmediate方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務老是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);
//不肯定

遞歸的調用process.nextTick()會致使I/O starving,官方推薦使用setImmediate()

process.nextTick(function foo() {
  process.nextTick(foo);
});
//FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory

process.nextTick也會放入microtask quque,爲何優先級比promise.then高呢
在Node中,_tickCallback在每一次執行完TaskQueue中的一個任務後被調用,而這個_tickCallback中實質上幹了兩件事:

  1. nextTickQueue中全部任務執行掉(長度最大1e4,Node版本v6.9.1)
  2. 第一步執行完後執行_runMicrotasks函數,執行microtask中的部分(promise.then註冊的回調)因此很明顯process.nextTick > promise.then」

clipboard.png
node.js的特色是事件驅動,非阻塞單線程。當應用程序須要I/O操做的時候,線程並不會阻塞,而是把I/O操做交給底層庫(LIBUV)。此時node線程會去處理其餘任務,當底層庫處理完I/O操做後,會將主動權交還給Node線程,因此Event Loop的用處是調度線程,例如:當底層庫處理I/O操做後調度Node線程處理後續工做,因此雖然node是單線程,可是底層庫處理操做依然是多線程。

根據Node.js官方介紹,每次事件循環都包含了6個階段,對應到 libuv 源碼中的實現,以下圖所示
clipboard.png

timers :這個階段執行timer(setTimeout、setInterval)的回調
I/O callbacks:執行一些系統調用錯誤,好比網絡通訊的錯誤回調
idle, prepare :僅node內部使用
poll :獲取新的I/O事件, 適當的條件下node將阻塞在這裏
check :執行 setImmediate() 的回調
close callbacks :執行 socket 的 close 事件回調

timers階段

timers 是事件循環的第一個階段,Node 會去檢查有無已過時的timer,若是有則把它的回調壓入timer的任務隊列中等待執行,事實上,Node 並不能保證timer在預設時間到了就會當即執行,由於Node對timer的過時檢查不必定靠譜,它會受機器上其它運行程序影響,或者那個時間點主線程不空閒。可是把它們放到一個I/O回調裏面,就必定是 setImmediate() 先執行,由於poll階段後面就是check階段。

I/O callbacks 階段

這個階段主要執行一些系統操做帶來的回調函數,如 TCP 錯誤,若是 TCP 嘗試連接時出現 ECONNREFUSED 錯誤 ,一些 *nix 會把這個錯誤報告給 Node.js。而這個錯誤報告會先進入隊列中,而後在 I/O callbacks 階段執行。

poll 階段

poll 階段主要有2個功能:

  • 處理 poll 隊列的事件
  • 當有已超時的 timer,執行它的回調函數

even loop將同步執行poll隊列裏的回調,直到隊列爲空或執行的回調達到系統上限(上限具體多少未詳),接下來even loop會去檢查有無預設的setImmediate(),分兩種狀況:

  1. 如有預設的setImmediate(), event loop將結束poll階段進入check階段,並執行check階段的任務隊列
  2. 若沒有預設的setImmediate(),event loop將阻塞在該階段等待

注意一個細節,沒有setImmediate()會致使event loop阻塞在poll階段,這樣以前設置的timer豈不是執行不了了?因此咧,在poll階段event loop會有一個檢查機制,檢查timer隊列是否爲空,若是timer隊列非空,event loop就開始下一輪事件循環,即從新進入到timer階段。

check 階段

setImmediate()的回調會被加入check隊列中, 從event loop的階段圖能夠知道,check階段的執行順序在poll階段以後。

close 階段

忽然結束的事件的回調函數會在這裏觸發,若是 socket.destroy(),那麼 close 會被觸發在這個階段,也有可能經過 process.nextTick() 來觸發。

示例

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
/*瀏覽器中
timer1
promise1
timer2
promise2
*/
/*node中
timer1
timer2
promise1
promise2
*/
const fs = require('fs')

fs.readFile('test.txt', () => {
  console.log('readFile')
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
/*
readFile
immediate
timeout
*/

更多示例
libuv源碼
https://github.com/libuv/libu...

其餘

requestAnimationFrame

HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。另外,對於那些DOM的變更(尤爲是涉及頁面從新渲染的部分),一般不會當即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果要好於setTimeout()

客戶端可能實現了一個包含鼠標鍵盤事件的任務隊列,還有其餘的任務隊列,而給鼠標鍵盤事件的任務隊列更高優先級,例如75%的可能性執行它。這樣就能保證流暢的交互性,並且別的任務也能執行到了。可是,同一個任務隊列中的任務必須按先進先出的順序執行。

用戶點擊與button.click()的區別:
用戶點擊:依次執行listener。瀏覽器並不實現知道有幾個 listener,所以它發現一個執行一個,執行完了再看後面還有沒有。
click:同步執行listener。 click方法會先採集有哪些 listener,再依次觸發。
示例詳情

參考資料
Promise的隊列與setTimeout的隊列有何關聯?
瀏覽器的 Event Loop
Event Loops
深刻理解js事件循環機制(Node.js篇)
JavaScript 運行機制詳解:再談Event Loop
Node.js 事件循環,定時器和 process.nextTick()

相關文章
相關標籤/搜索