理解event loop(瀏覽器環境與nodejs環境)

轉自IMWeb社區,做者:sugerpocket,原文連接javascript

衆所周知,javascript 是單線程的,其經過使用異步而不阻塞主進程執行。那麼,他是如何實現的呢?本文就瀏覽器與nodejs環境下異步實現與event loop進行相關解釋。java

瀏覽器環境

瀏覽器環境下,會維護一個任務隊列,當異步任務到達的時候加入隊列,等待事件循環到合適的時機執行。node

實際上,js 引擎並不僅維護一個任務隊列,總共有兩種任務git

  1. Task(macroTask): setTimeout, setInterval, setImmediate,I/O, UI rendering
  2. microTask: Promise, process.nextTick, Object.observe, MutationObserver, MutaionObserver

那麼兩種任務的行爲有何不一樣呢?github

實驗一下,請看下段代碼web

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

var promise = 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);
複製代碼

輸出:shell

1 2 3 5 4
複製代碼

這說明 Promise.then 註冊的任務先執行了。編程

咱們再來看一下以前說的 Promise 註冊的任務屬於microTask,setTimeout 屬於 Task,二者有何差異?windows

實際上,microTasksTasks 並不在同一個隊列裏面,他們的調度機制也不相同。比較具體的是這樣:promise

  1. event-loop start
  2. microTasks 隊列開始清空(執行)
  3. 檢查 Tasks 是否清空,有則跳到 4,無則跳到 6
  4. 從 Tasks 隊列抽取一個任務,執行
  5. 檢查 microTasks 是否清空,如有則跳到 2,無則跳到 3
  6. 結束 event-loop

也就是說,microTasks 隊列在一次事件循環裏面不止檢查一次,咱們作個實驗

// 添加三個 Task
// Task 1
setTimeout(function() {
  console.log(4);
}, 0);

// Task 2
setTimeout(function() {
  console.log(6);
  // 添加 microTask
  promise.then(function() {
    console.log(8);
  });
}, 0);

// Task 3
setTimeout(function() {
  console.log(7);
}, 0);

var promise = 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 6 8 7
複製代碼

microTasks 會在每一個 Task 執行完畢以後檢查清空,而此次 event-loop 的新 task 會在下次 event-loop 檢測。

Node 環境

實際上,node.js環境下,異步的實現根據操做系統的不一樣而有所差別。而不一樣的異步方式處理確定也是不相同的,其並無嚴格按照js單線程的原則,運行環境有可能會經過其餘線程完成異步,固然,js引擎仍是單線程的。

node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js將事件驅動的I/O模型與適合該模型的編程語言(Javascript)融合在了一塊兒。隨着node.js的日益流行,node.js須要同時支持windows, 可是libev只能在Unix環境下運行。Windows 平臺上與kqueue(FreeBSD)或者(e)poll(Linux)等內核事件通知相應的機制是IOCP。libuv提供了一個跨平臺的抽象,由平臺決定使用libev或IOCP。

關於event loop,node.js 環境下與瀏覽器環境有着巨大差別。

先來一張圖

先解釋一下各個階段

  1. timers: 這個階段執行setTimeout()和setInterval()設定的回調。
  2. I/O callbacks: 執行幾乎全部的回調,除了close回調,timer的回調,和setImmediate()的回調。
  3. idle, prepare: 僅內部使用。
  4. poll: 獲取新的I/O事件;node會在適當條件下阻塞在這裏。
  5. check: 執行setImmediate()設定的回調。
  6. close callbacks: 執行好比socket.on('close', ...)的回調。

每一個階段的詳情

timer

一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回調。在指定時間事後,timers會盡量早地執行回調,但系統調度或者其它回調的執行可能會延遲它們。

注意:技術上來講,poll 階段控制 timers 何時執行。

I/O callbacks 這個階段執行一些系統操做的回調。好比TCP錯誤,如一個TCP socket在想要鏈接時收到ECONNREFUSED, 類unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執行。

poll

poll 階段的功能有兩個

  • 執行 timer 階段到達時間上限的任務。
  • 執行 poll 階段的任務隊列。

若是進入 poll 階段,而且沒有 timer 階段加入的任務,將會發生如下狀況

  • 若是 poll 隊列不爲空的話,會執行 poll 隊列直到清空或者系統回調數達到上限
  • 若是 poll 隊列爲空
    若是設定了 setImmediate 回調,會直接跳到 check 階段。 若是沒有設定 setImmediate 回調,會阻塞住進程,並等待新的 poll 任務加入並當即執行。
check

這個階段在 poll 結束後當即執行,setImmediate 的回調會在這裏執行。

通常來講,event loop 確定會進入 poll 階段,當沒有 poll 任務時,會等待新的任務出現,但若是設定了 setImmediate,會直接執行進入下個階段而不是繼續等。

close

close 事件在這裏觸發,不然將經過 process.nextTick 觸發。

一個例子
var fs = require('fs');

function someAsyncOperation (callback) {
  // 假設這個任務要消耗 95ms
  fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

  var delay = Date.now() - timeoutScheduled;

  console.log(delay + "ms have passed since I was scheduled");
}, 100);


// someAsyncOperation要消耗 95 ms 才能完成
someAsyncOperation(function () {

  var startCallback = Date.now();

  // 消耗 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});
複製代碼

當event loop進入 poll 階段,它有個空隊列(fs.readFile()還沒有結束)。因此它會等待剩下的毫秒, 直到最近的timer的下限時間到了。當它等了95ms,fs.readFile()首先結束了,而後它的回調被加到 poll 的隊列並執行——這個回調耗時10ms。以後因爲沒有其它回調在隊列裏,因此event loop會查看最近達到的timer的 下限時間,而後回到 timers 階段,執行timer的回調。

因此在示例裏,回調被設定 和 回調執行間的間隔是105ms。

setImmediate() vs setTimeout()

如今咱們應該知道二者的不一樣,他們的執行階段不一樣,setImmediate() 在 check 階段,而settimeout 在 poll 階段執行。但,還不夠。來看一下例子。

// timeout_vs_immediate.js
setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
複製代碼
$ node timeout_vs_immediate.js
timeout
immediate
 $ node timeout_vs_immediate.js
immediate
timeout
複製代碼

結果竟然是不肯定的,why?

仍是直接給出解釋吧。

  1. 首先進入timer階段,若是咱們的機器性能通常,那麼進入timer階段時,1毫秒可能已通過去了(setTimeout(fn, 0) 等價於setTimeout(fn, 1)),那麼setTimeout的回調會首先執行。
  2. 若是沒到一毫秒,那麼咱們能夠知道,在check階段,setImmediate的回調會先執行。

那咱們再來一個

// timeout_vs_immediate.js
var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
複製代碼

輸出始終爲

$ node timeout_vs_immediate.js
immediate
timeout
複製代碼

這個就很好解釋了吧。 fs.readFile 的回調執行是在 poll 階段。當 fs.readFile 回調執行完畢以後,會直接到 check 階段,先執行 setImmediate 的回調。

process.nextTick()

nextTick 比較特殊,它有本身的隊列,而且,獨立於event loop。 它的執行也很是特殊,不管 event loop 處於何種階段,都會在階段結束的時候清空 nextTick 隊列。

參考

juejin.im/entry/58332… jakearchibald.com/2015/tasks-… flyyang.github.io/2017/03/07/… hao5743.github.io/2017/02/27/… github.com/ccforward/c… github.com/creeperyang… developer.mozilla.org/zh-CN/docs/…

相關文章
相關標籤/搜索