原文出自:https://www.pandashen.com
瀏覽器中的事件輪詢
JavaScript 是一門單線程語言,之所以說是單線程,是因爲在瀏覽器中,如果是多線程,並且兩個線程同時操作了同一個 Dom 元素,那最後的結果會出現問題。所以,JavaScript 是單線程的,但是如果完全由上至下的一行一行執行代碼,假如一個代碼塊執行了很長的時間,後面必須要等待當前執行完畢,這樣的效率是非常低的,所以有了異步的概念,確切的說,JavaScript 的主線程是單線程的,但是也有其他的線程去幫我們實現異步操作,比如定時器線程、事件線程、Ajax 線程。
在瀏覽器中執行 JavaScript 有兩個區域,一個是我們平時所說的同步代碼執行,是在棧中執行,原則是先進後出,而在執行異步代碼的時候分爲兩個隊列,macro-task
(宏任務)和 micro-task
(微任務),遵循先進先出的原則。
// 作用域鏈
function one() {
console.log(1);
function two() {
console.log(2);
function three() {
console.log(3);
}
three();
}
two();
}
one();
// 1
// 2
// 3
上面的代碼都是同步的代碼,在執行的時候先將全局作用域放入棧中,執行全局作用域中的代碼,解析了函數 one
,當執行函數調用 one()
的時候將 one
的作用域放入棧中,執行 one
中的代碼,打印了 1
,解析了 two
,執行 two()
,將 two
放入棧中,執行 two
,打印了 2
,解析了 three
,執行了 three()
,將 three
放入棧中,執行 three
,打印了 3
。
在函數執行完釋放的過程中,因爲全局作用域中有 one
正在執行,one
中有 two
正在執行,two
中有 three
正在執行,所以釋放內存時必須由內層向外層釋放,three
執行後釋放,此時 three
不再佔用 two
的執行環境,將 two
釋放,two
不再佔用 one
的執行環境,將 one
釋放,one
不再佔用全局作用域的執行環境,最後釋放全局作用域,這就是在棧中執行同步代碼時的先進後出原則,更像是一個杯子,先放進去的在最下面,需要最後取出。
而異步隊列更像時一個管道,有兩個口,從入口進,從出口出,所以是先進先出,在宏任務隊列中代表的有 setTimeout
、setInterval
、setImmediate
、MessageChannel
,微任務的代表爲 Promise 的 then
方法、MutationObserve
(已廢棄)。
案例 1
let messageChannel = new MessageChannel();
let prot2 = messageChannel.port2;
messageChannel.port1.postMessage("I love you");
console.log(1);
prot2.onmessage = function(e) {
console.log(e.data);
};
console.log(2);
// 1
// 2
// I love you
從上面案例中可以看出,MessageChannel
是宏任務,晚於同步代碼執行。
案例 2
setTimeout(() => console.log(1), 2000);
setTimeout(() => console.log(2), 1000);
console.log(3);
// 3
// 2
// 1
上面代碼可以看出其實 setTimeout
並不是在同步代碼執行的時候就放入了異步隊列,而是等待時間到達時纔會放入異步隊列,所以纔會有了上面的結果。
案例 3
setImmediate(function() {
console.log("setImmediate");
});
setTimeout(function() {
console.log("setTimeout");
}, 0);
console.log(1);
// 1
// setTimeout
// setImmediate
同爲宏任務,setImmediate
在 setTimeout
延遲時間爲 0
時是晚於 setTimeout
被放入異步隊列的,這裏需要注意的是 setImmediate
在瀏覽器端,到目前爲止只有 IE 實現了。
上面的案例都是關於宏任務,下面我們舉一個有微任務的案例來看一看微任務和宏任務的執行機制,在瀏覽器端微任務的代表其實就是 Promise 的 then
方法。
案例 4
setTimeout(() => {
console.log("setTimeout1");
Promise.resolve().then(data => {
console.log("Promise1");
});
}, 0);
Promise.resolve().then(data => {
console.log("Promise2");
setTimeout(() => {
console.log("setTimeout2");
}, 0);
});
// Promise2
// setTimeout1
// Promise1
// setTimeout2
從上面的執行結果其實可以看出,同步代碼在棧中執行完畢後會先去執行微任務隊列,將微任務隊列執行完畢後,會去執行宏任務隊列,宏任務隊列執行一個宏任務以後,會去看看有沒有產生新的微任務,如果有則清空微任務隊列後再執行下一個宏任務,依次輪詢,直到清空整個異步隊列。
Node 中的事件輪詢
在 Node 中的事件輪詢機制與瀏覽器相似又不同,相似的是,同樣先在棧中執行同步代碼,同樣是先進後出,不同的是 Node 有自己的多個處理不同問題的階段和對應的隊列,也有自己內部實現的微任務 process.nextTick
,Node 的整個事件輪詢機制是 Libuv 庫實現的。
Node 中事件輪詢的流程如下圖:
從圖中可以看出,在 Node 中有多個隊列,分別執行不同的操作,而每次在隊列切換的時候都去執行一次微任務隊列,反覆的輪詢。
案例 1
setTimeout(function() {
console.log("setTimeout");
}, 0);
setImmediate(function() {
console.log("setInmediate");
});
默認情況下 setTimeout
和 setImmediate
是不知道哪一個先執行的,順序不固定,Node 執行的時候有準備的時間,setTimeout
延遲時間設置爲 0
其實是大概 4ms
,假設 Node 準備時間在 4ms
之內,開始執行輪詢,定時器沒到時間,所以輪詢到下一隊列,此時要等再次循環到 timer
隊列後執行定時器,所以會先執行 check
隊列的 setImmediate
。
如果 Node 執行的準備時間大於了 4ms
,因爲執行同步代碼後,定時器的回調已經被放入 timer
隊列,所以會先執行 timer
隊列。
案例 2
setTimeout(() => {
console.log("setTimeout1");
Promise.resolve().then(() => {
console.log("Promise1");
});
}, 0);
setTimeout(() => {
console.log("setTimeout2");
}, 0);
console.log(1);
// 1
// setTimeout1
// setTimeout2
// Promise1
Node 事件輪詢中,輪詢到每一個隊列時,都會將當前隊列任務清空後,在切換下一隊列之前清空一次微任務隊列,這是與瀏覽器端不一樣的。
瀏覽器端會在宏任務隊列當中執行一個任務後插入執行微任務隊列,清空微任務隊列後,再回到宏任務隊列執行下一個宏任務。
上面案例在 Node 事件輪詢中,會將 timer
隊列清空後,在輪詢下一個隊列之前執行微任務隊列。
案例 3
setTimeout(() => {
console.log("setTimeout1");
}, 0);
setTimeout(() => {
console.log("setTimeout2");
}, 0);
Promise.resolve().then(() => {
console.log("Promise1");
});
console.log(1);
// 1
// Promise1
// setTimeout1
// setTimeout2
上面代碼的執行過程是,先執行棧,棧執行時打印 1
,Promise.resolve()
產生微任務,棧執行完畢,從棧切換到 timer
隊列之前,執行微任務隊列,再去執行 timer
隊列。
案例 4
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
//結果1
// setImmediate1
// setTimeout2
// setTimeout1
// setImmediate2
// 結果2
// setTimeout2
// setImmediate1
// setImmediate2
// setTimeout1
setImmediate
和 setTimeout
執行順序不固定,假設 check
隊列先執行,會執行 setImmediate
打印 setImmediate1
,將遇到的定時器放入 timer
隊列,輪詢到 timer
隊列,因爲在棧中執行同步代碼已經在 timer
隊列放入了一個定時器,所以按先後順序執行兩個 setTimeout
,執行第一個定時器打印 setTimeout2
,將遇到的 setImmediate
放入 check
隊列,執行第二個定時器打印 setTimeout1
,再次輪詢到 check
隊列執行新加入的 setImmediate
,打印 setImmediate2
,產生結果 1
。
假設 timer
隊列先執行,會執行 setTimeout
打印 setTimeout2
,將遇到的 setImmediate
放入 check
隊列,輪詢到 check
隊列,因爲在棧中執行同步代碼已經在 check
隊列放入了一個 setImmediate
,所以按先後順序執行兩個 setImmediate
,執行第一個 setImmediate
打印 setImmediate1
,將遇到的 setTimeout
放入 timer
隊列,執行第二個 setImmediate
打印 setImmediate2
,再次輪詢到 timer
隊列執行新加入的 setTimeout
,打印 setTimeout1
,產生結果 2
。
案例 5
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
//結果1
// setImmediate1
// setTimeout2
// setTimeout1
// nextTick
// setImmediate2
// 結果2
// setTimeout2
// nextTick
// setImmediate1
// setImmediate2
// setTimeout1
這與上面一個案例類似,不同的是在 setTimeout
執行的時候產生了一個微任務 nextTick
,我們只要知道,在 Node 事件輪詢中,在切換隊列時要先去執行微任務隊列,無論是 check
隊列先執行,還是 timer
隊列先執行,都會很容易分析出上面的兩個結果。
案例 6
const fs = require("fs");
fs.readFile("./.gitignore", "utf8", function() {
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(function() {
console.log("setImmediate");
});
});
// setImmediate
// timeout
上面案例的 setTimeout
和 setImmediate
的執行順序是固定的,前面都是不固定的,這是爲什麼?
因爲前面的不固定是在棧中執行同步代碼時就遇到了 setTimeout
和 setImmediate
,因爲無法判斷 Node 的準備時間,不確定準備結束定時器是否到時並加入 timer
隊列。
而上面代碼明顯可以看出 Node 準備結束後會直接執行 poll
隊列進行文件的讀取,在回調中將 setTimeout
和 setImmediate
分別加入 timer
隊列和 check
隊列,Node 隊列的輪詢是有順序的,在 poll
隊列後應該先切換到 check
隊列,然後再重新輪詢到 timer
隊列,所以得到上面的結果。
案例 7
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));
// nextTick
// Promise
在 Node 中有兩個微任務,Promise
的 then
方法和 process.nextTick
,從上面案例的結果我們可以看出,在微任務隊列中 process.nextTick
是優先執行的。
上面內容就是瀏覽器與 Node 在事件輪詢的規則,相信在讀完以後應該已經徹底弄清了瀏覽器的事件輪詢機制和 Node 的事件輪詢機制,並深刻的體會到了他們之間的相同和不同。