03-JS單執行緒, event loop徹底搞懂程式碼執行順序
瀏覽器工作原理
單執行緒的含義
瀏覽器是 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