單線程的含義
瀏覽器是 multi-process,一個瀏覽器只有一個 Browser Process,負責管理 Tabs、協調其他 process 和 Renderer process 存至 memory 內的 Bitmap 繪製到頁面上的(pixel);在 Chrome中,一個 Tab 對應一個 Renderer Process,Renderer process 是 multi-thread,其中 main thread 負責頁面渲染(GUI render engine)執行 JS (JS engine)和 event loop;network component 可以開2~6個 I/O threads 平行去處理。
Structure of a Web Browser
主線程,JS執行線程,UI渲染線程關係如下圖所示:
瀏覽器中的 JavaScript 執行機制
可視化演繹
深入演示:loupe
https://github.com/latentflip/loupe
// 函數執行棧演繹-->函數調用過程
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();複製代碼
兩個問題
問題1:如果我們在瀏覽器控制檯中運行'foo'函數,是否會導致堆棧溢出錯誤?
function foo() {
setTimeout(foo, 0); // 是否存在堆棧溢出錯誤?
};複製代碼
function foo() {
foo() // 是否存在堆棧溢出錯誤?
};
foo();複製代碼
問題2:如果在控制檯中運行以下函數,頁面(選項卡)的 UI 是否仍然響應
function foo() {
return Promise.resolve().then(foo);
};複製代碼
基礎題
alert(x);
var x = 10;
alert(x);
x = 20;
function x() {};
alert(x); 複製代碼
瀏覽器端的 Event Loop
一個函數執行棧、一個事件隊列和一個微任務隊列。
每從事件隊列中取一個事件時有微任務就把微任務執行完,然後纔開始執行事件
宏任務和微任務
宏任務,macrotask,也叫tasks。 一些異步任務的回調會依次進入macro task queue,等待後續被調用,這些異步任務包括:
- setTimeout
- setInterval
- setImmediate (Node獨有)
- requestAnimationFrame (瀏覽器獨有)
- I/O
- UI rendering (瀏覽器獨有)
微任務,microtask,也叫jobs。 另一些異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包括:
- process.nextTick (Node獨有)
- Promise.then()
- Object.observe
- MutationObserver
(注:這裏只針對瀏覽器和NodeJS)
注意:Promise構造函數裏的代碼是同步執行的。
基礎題
setTimeout(()=> {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)
setTimeout(()=> {
console.log(2)
}, 0)複製代碼
可視化演繹
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});複製代碼
瀏覽器端:jakearchibald.com/2015/tasks-…
鞏固提高題
console.time("start")
setTimeout(function () {
console.log(2);
}, 10);
new Promise(function (resolve) {
console.log(3);
resolve();
console.log(4);
}).then(function () {
console.log(5);
console.timeEnd("start")
});
console.log(6);
console.log(8);
requestAnimationFrame(() => console.log(9))複製代碼
Node.js 架構圖
Node.js 中的 Event Loop
Node.js的Event Loop過程:
- 執行全局Script的同步代碼
- 執行microtask微任務,先執行所有Next Tick Queue中的所有任務,再執行Other Microtask Queue中的所有任務
- 開始執行macrotask宏任務,共6個階段,從第1個階段開始執行相應每一個階段macrotask中的所有任務,注意,這裏是所有每個階段宏任務隊列的所有任務,在瀏覽器的Event Loop中是隻取宏隊列的第一個任務出來執行,每一個階段的macrotask任務執行完畢後,開始執行微任務,也就是步驟2
- Timers Queue -> 步驟2 -> I/O Queue -> 步驟2 -> Check Queue -> 步驟2 -> Close Callback Queue -> 步驟2 -> Timers Queue ......
- 這就是Node的Event Loop【簡化版】
瀏覽器端和 Node 端有什麼不同
- 瀏覽器的Event Loop和Node.js 的Event Loop是不同的,實現機制也不一樣,不要混爲一談。
- Node.js 可以理解成有4個宏任務隊列和2個微任務隊列,但是執行宏任務時有6個階段。
- Node.js 中,先執行全局Script代碼,執行完同步代碼調用棧清空後,先從微任務隊列Next Tick Queue中依次取出所有的任務放入調用棧中執行,再從微任務隊列Other Microtask Queue中依次取出所有的任務放入調用棧中執行。然後開始宏任務的6個階段,每個階段都將該宏任務隊列中的所有任務都取出來執行(注意,這裏和瀏覽器不一樣,瀏覽器只取一個),每個宏任務階段執行完畢後,開始執行微任務,再開始執行下一階段宏任務,以此構成事件循環。
- MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering
- Microtask包括: process.nextTick(Node)、Promise.then、Object.observe、MutationObserver
注意:new Promise() 構造函數裏面是同步代碼,而非微任務。
面試常考細節
微任務有兩種 nextTick和 then 那麼這兩個誰快呢?
Promise.resolve('123').then(res=>{ console.log(res)})
process.nextTick(() => console.log('nextTick'))複製代碼
//順序 nextTick 123
//很明顯 nextTick快
解釋:
promise.then 雖然和 process.nextTick 一樣,都將回調函數註冊到 microtask,但優先級不一樣。process.nextTick 的 microtask queue 總是優先於 promise 的 microtask queue 執行。
setTimeout 和 setImmediate
setImmediate(callback[, ...args])
Schedules the "immediate" execution of the callback
after I/O events' callbacks.
setImmediate()方法用於中斷長時間運行的操作,並在完成其他操作後立即運行回調函數。
setTimeout 和 setImmediate 執行順序不固定 取決於node的準備時間
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})複製代碼
運行結果:
setImmediate
setTimeout
或者:
setTimeout
setImmediate
爲什麼結果不確定呢?
解釋:
setTimeout/setInterval 的第二個參數取值範圍是:[1, 2^31 - 1],如果超過這個範圍則會初始化爲 1,
即 setTimeout(fn, 0) === setTimeout(fn, 1)。
我們知道 setTimeout 的回調函數在 timer 階段執行,setImmediate 的回調函數在 check 階段執行,event loop 的開始會先檢查 timer 階段,但是在開始之前到 timer 階段會消耗一定時間;
所以就會出現兩種情況:
- timer 前的準備時間超過 1ms,滿足 loop->time >= 1,則執行 timer 階段(setTimeout)的回調函數
- timer 前的準備時間小於 1ms,則先執行 check 階段(setImmediate)的回調函數,下一次 event loop 執行 timer 階段(setTimeout)的回調函數。
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
const start = Date.now()
while (Date.now() - start < 10);複製代碼
運行結果一定是:
setTimeout
setImmediate
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})複製代碼
運行結果:
setImmediate
setTimeout
解釋:
fs.readFile 的回調函數執行完後:
註冊 setTimeout 的回調函數到 timer 階段
註冊 setImmediate 的回調函數到 check 階段
event loop 從 pool 階段出來繼續往下一個階段執行,恰好是 check 階段,所以 setImmediate 的回調函數先執行
本次 event loop 結束後,進入下一次 event loop,執行 setTimeout 的回調函數
所以,在 I/O Callbacks 中註冊的 setTimeout 和 setImmediate,永遠都是 setImmediate 先執行。
鞏固提高題目
console.time("start")
setTimeout(function () {
console.log(2);
}, 10);
setImmediate(function () {
console.log(1);
});
new Promise(function (resolve) {
console.log(3);
resolve();
console.log(4);
}).then(function () {
console.log(5);
console.timeEnd("start")
});
console.log(6);
process.nextTick(function () {
console.log(7);
});
console.log(8);
// requestAnimationFrame(() => console.log(9))
複製代碼
運行結果如下:
運行時分析
Node 11.x + 新變化
setTimeout(() => console.log('timeout1'));
setTimeout(() => {
console.log('timeout2')
Promise.resolve().then(() => console.log('promise resolve'))
});
setTimeout(() => console.log('timeout3'));
setTimeout(() => console.log('timeout4'));複製代碼
瀏覽器執行結果:
低於Node 11的版本
Node 11+
向瀏覽器運行結果靠齊
參考資料:
github.com/nodejs/node… MacroTask and MicroTask execution order
blog.insiderattack.net/new-changes…
github.com/nodejs/node… timers: run nextTicks after each immediate and timer