通殺 Event Loop 面試題

自由如風FC發表於2019-08-12

03-JS單執行緒, event loop徹底搞懂程式碼執行順序

瀏覽器工作原理

通殺 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

通殺 Event Loop 面試題image.png

主執行緒,JS執行執行緒,UI渲染執行緒關係如下圖所示:

通殺 Event Loop 面試題

通殺 Event Loop 面試題

瀏覽器中的 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

一個函式執行棧、一個事件佇列和一個微任務佇列。

每從事件佇列中取一個事件時有微任務就把微任務執行完,然後才開始執行事件


通殺 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 架構圖

image.png

Node.js 中的 Event Loop

通殺 Event Loop 面試題


通殺 Event Loop 面試題通殺 Event Loop 面試題node_eventloop.png


Node.js的Event Loop過程:

  1. 執行全域性Script的同步程式碼
  2. 執行microtask微任務,先執行所有Next Tick Queue中的所有任務,再執行Other Microtask Queue中的所有任務
  3. 開始執行macrotask巨集任務,共6個階段,從第1個階段開始執行相應每一個階段macrotask中的所有任務,注意,這裡是所有每個階段巨集任務佇列的所有任務,在瀏覽器的Event Loop中是隻取巨集佇列的第一個任務出來執行,每一個階段的macrotask任務執行完畢後,開始執行微任務,也就是步驟2
  4. Timers Queue -> 步驟2 -> I/O Queue -> 步驟2 -> Check Queue -> 步驟2 -> Close Callback Queue -> 步驟2 -> Timers Queue ......
  5. 這就是Node的Event Loop【簡化版】


通殺 Event Loop 面試題


瀏覽器端和 Node 端有什麼不同

  1. 瀏覽器的Event Loop和Node.js 的Event Loop是不同的,實現機制也不一樣,不要混為一談。
  2. Node.js 可以理解成有4個巨集任務佇列和2個微任務佇列,但是執行巨集任務時有6個階段。
  3. Node.js 中,先執行全域性Script程式碼,執行完同步程式碼呼叫棧清空後,先從微任務佇列Next Tick Queue中依次取出所有的任務放入呼叫棧中執行,再從微任務佇列Other Microtask Queue中依次取出所有的任務放入呼叫棧中執行。然後開始巨集任務的6個階段,每個階段都將該巨集任務佇列中的所有任務都取出來執行(注意,這裡和瀏覽器不一樣,瀏覽器只取一個),每個巨集任務階段執行完畢後,開始執行微任務,再開始執行下一階段巨集任務,以此構成事件迴圈。
  4. MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering
  5. 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 階段會消耗一定時間;

所以就會出現兩種情況:

  1. timer 前的準備時間超過 1ms,滿足 loop->time >= 1,則執行 timer 階段(setTimeout)的回撥函式
  2. 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))
複製程式碼

執行結果如下:

image.png

執行時分析

image.png


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'));複製程式碼

瀏覽器執行結果:

image.png

低於Node 11的版本

image.png

Node 11+

image.png

向瀏覽器執行結果靠齊

image.png

image.png

參考資料:

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

技術交流群:668337232

通殺 Event Loop 面試題

掃碼聯絡我哦~

通殺 Event Loop 面試題


相關文章