衆所周知,javascript 是單線程的,其經過使用異步而不阻塞主進程執行。那麼,他是如何實現的呢?本文就瀏覽器與nodejs環境下異步實現與event loop進行相關解釋。java
瀏覽器環境下,會維護一個任務隊列,當異步任務到達的時候加入隊列,等待事件循環到合適的時機執行。node
實際上,js 引擎並不僅維護一個任務隊列,總共有兩種任務git
setTimeout
, setInterval
, setImmediate
,I/O
, UI rendering
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
實際上,microTasks
和 Tasks
並不在同一個隊列裏面,他們的調度機制也不相同。比較具體的是這樣:promise
也就是說,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.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 環境下與瀏覽器環境有着巨大差別。
先來一張圖
一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回調。在指定時間事後,timers會盡量早地執行回調,但系統調度或者其它回調的執行可能會延遲它們。
注意:技術上來講,poll 階段控制 timers 何時執行。
I/O callbacks 這個階段執行一些系統操做的回調。好比TCP錯誤,如一個TCP socket在想要鏈接時收到ECONNREFUSED, 類unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執行。
poll 階段的功能有兩個
若是進入 poll 階段,而且沒有 timer 階段加入的任務,將會發生如下狀況
這個階段在 poll 結束後當即執行,setImmediate 的回調會在這裏執行。
通常來講,event loop 確定會進入 poll 階段,當沒有 poll 任務時,會等待新的任務出現,但若是設定了 setImmediate,會直接執行進入下個階段而不是繼續等。
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() 在 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?
仍是直接給出解釋吧。
那咱們再來一個
// 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 的回調。
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/…