原文出自:https://www.pandashen.com
瀏覽器中的事件輪詢
JavaScript 是一門單線程語言,之所以說是單線程,是因?yàn)樵跒g覽器中,如果是多線程,并且兩個(gè)線程同時(shí)操作了同一個(gè) Dom 元素馆匿,那最后的結(jié)果會(huì)出現(xiàn)問題。所以燥滑,JavaScript 是單線程的渐北,但是如果完全由上至下的一行一行執(zhí)行代碼,假如一個(gè)代碼塊執(zhí)行了很長(zhǎng)的時(shí)間铭拧,后面必須要等待當(dāng)前執(zhí)行完畢赃蛛,這樣的效率是非常低的,所以有了異步的概念搀菩,確切的說呕臂,JavaScript 的主線程是單線程的,但是也有其他的線程去幫我們實(shí)現(xiàn)異步操作肪跋,比如定時(shí)器線程歧蒋、事件線程、Ajax 線程州既。
在瀏覽器中執(zhí)行 JavaScript 有兩個(gè)區(qū)域谜洽,一個(gè)是我們平時(shí)所說的同步代碼執(zhí)行,是在棧中執(zhí)行易桃,原則是先進(jìn)后出褥琐,而在執(zhí)行異步代碼的時(shí)候分為兩個(gè)隊(duì)列锌俱,macro-task
(宏任務(wù))和 micro-task
(微任務(wù))晤郑,遵循先進(jìn)先出的原則。
// 作用域鏈
function one() {
console.log(1);
function two() {
console.log(2);
function three() {
console.log(3);
}
three();
}
two();
}
one();
// 1
// 2
// 3
上面的代碼都是同步的代碼贸宏,在執(zhí)行的時(shí)候先將全局作用域放入棧中造寝,執(zhí)行全局作用域中的代碼,解析了函數(shù) one
吭练,當(dāng)執(zhí)行函數(shù)調(diào)用 one()
的時(shí)候?qū)?one
的作用域放入棧中诫龙,執(zhí)行 one
中的代碼,打印了 1
鲫咽,解析了 two
签赃,執(zhí)行 two()
谷异,將 two
放入棧中,執(zhí)行 two
锦聊,打印了 2
歹嘹,解析了 three
,執(zhí)行了 three()
孔庭,將 three
放入棧中尺上,執(zhí)行 three
,打印了 3
圆到。
在函數(shù)執(zhí)行完釋放的過程中怎抛,因?yàn)槿肿饔糜蛑杏?one
正在執(zhí)行,one
中有 two
正在執(zhí)行芽淡,two
中有 three
正在執(zhí)行马绝,所以釋放內(nèi)存時(shí)必須由內(nèi)層向外層釋放,three
執(zhí)行后釋放吐绵,此時(shí) three
不再占用 two
的執(zhí)行環(huán)境迹淌,將 two
釋放,two
不再占用 one
的執(zhí)行環(huán)境己单,將 one
釋放唉窃,one
不再占用全局作用域的執(zhí)行環(huán)境,最后釋放全局作用域纹笼,這就是在棧中執(zhí)行同步代碼時(shí)的先進(jìn)后出原則纹份,更像是一個(gè)杯子,先放進(jìn)去的在最下面廷痘,需要最后取出蔓涧。
而異步隊(duì)列更像時(shí)一個(gè)管道,有兩個(gè)口笋额,從入口進(jìn)元暴,從出口出,所以是先進(jìn)先出兄猩,在宏任務(wù)隊(duì)列中代表的有 setTimeout
茉盏、setInterval
、setImmediate
枢冤、MessageChannel
鸠姨,微任務(wù)的代表為 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
是宏任務(wù),晚于同步代碼執(zhí)行核蘸。
案例 2
setTimeout(() => console.log(1), 2000);
setTimeout(() => console.log(2), 1000);
console.log(3);
// 3
// 2
// 1
上面代碼可以看出其實(shí) setTimeout
并不是在同步代碼執(zhí)行的時(shí)候就放入了異步隊(duì)列巍糯,而是等待時(shí)間到達(dá)時(shí)才會(huì)放入異步隊(duì)列啸驯,所以才會(huì)有了上面的結(jié)果。
案例 3
setImmediate(function() {
console.log("setImmediate");
});
setTimeout(function() {
console.log("setTimeout");
}, 0);
console.log(1);
// 1
// setTimeout
// setImmediate
同為宏任務(wù)祟峦,setImmediate
在 setTimeout
延遲時(shí)間為 0
時(shí)是晚于 setTimeout
被放入異步隊(duì)列的坯汤,這里需要注意的是 setImmediate
在瀏覽器端,到目前為止只有 IE 實(shí)現(xiàn)了搀愧。
上面的案例都是關(guān)于宏任務(wù)惰聂,下面我們舉一個(gè)有微任務(wù)的案例來看一看微任務(wù)和宏任務(wù)的執(zhí)行機(jī)制,在瀏覽器端微任務(wù)的代表其實(shí)就是 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
從上面的執(zhí)行結(jié)果其實(shí)可以看出搓幌,同步代碼在棧中執(zhí)行完畢后會(huì)先去執(zhí)行微任務(wù)隊(duì)列,將微任務(wù)隊(duì)列執(zhí)行完畢后迅箩,會(huì)去執(zhí)行宏任務(wù)隊(duì)列溉愁,宏任務(wù)隊(duì)列執(zhí)行一個(gè)宏任務(wù)以后,會(huì)去看看有沒有產(chǎn)生新的微任務(wù)饲趋,如果有則清空微任務(wù)隊(duì)列后再執(zhí)行下一個(gè)宏任務(wù)拐揭,依次輪詢,直到清空整個(gè)異步隊(duì)列奕塑。
Node 中的事件輪詢
在 Node 中的事件輪詢機(jī)制與瀏覽器相似又不同堂污,相似的是,同樣先在棧中執(zhí)行同步代碼龄砰,同樣是先進(jìn)后出盟猖,不同的是 Node 有自己的多個(gè)處理不同問題的階段和對(duì)應(yīng)的隊(duì)列,也有自己內(nèi)部實(shí)現(xiàn)的微任務(wù) process.nextTick
换棚,Node 的整個(gè)事件輪詢機(jī)制是 Libuv 庫實(shí)現(xiàn)的式镐。
Node 中事件輪詢的流程如下圖:
從圖中可以看出,在 Node 中有多個(gè)隊(duì)列固蚤,分別執(zhí)行不同的操作娘汞,而每次在隊(duì)列切換的時(shí)候都去執(zhí)行一次微任務(wù)隊(duì)列,反復(fù)的輪詢夕玩。
案例 1
setTimeout(function() {
console.log("setTimeout");
}, 0);
setImmediate(function() {
console.log("setInmediate");
});
默認(rèn)情況下 setTimeout
和 setImmediate
是不知道哪一個(gè)先執(zhí)行的你弦,順序不固定,Node 執(zhí)行的時(shí)候有準(zhǔn)備的時(shí)間风秤,setTimeout
延遲時(shí)間設(shè)置為 0
其實(shí)是大概 4ms
鳖目,假設(shè) Node 準(zhǔn)備時(shí)間在 4ms
之內(nèi)扮叨,開始執(zhí)行輪詢缤弦,定時(shí)器沒到時(shí)間,所以輪詢到下一隊(duì)列彻磁,此時(shí)要等再次循環(huán)到 timer
隊(duì)列后執(zhí)行定時(shí)器碍沐,所以會(huì)先執(zhí)行 check
隊(duì)列的 setImmediate
狸捅。
如果 Node 執(zhí)行的準(zhǔn)備時(shí)間大于了 4ms
,因?yàn)閳?zhí)行同步代碼后累提,定時(shí)器的回調(diào)已經(jīng)被放入 timer
隊(duì)列尘喝,所以會(huì)先執(zhí)行 timer
隊(duì)列。
案例 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 事件輪詢中斋陪,輪詢到每一個(gè)隊(duì)列時(shí)朽褪,都會(huì)將當(dāng)前隊(duì)列任務(wù)清空后,在切換下一隊(duì)列之前清空一次微任務(wù)隊(duì)列无虚,這是與瀏覽器端不一樣的缔赠。
瀏覽器端會(huì)在宏任務(wù)隊(duì)列當(dāng)中執(zhí)行一個(gè)任務(wù)后插入執(zhí)行微任務(wù)隊(duì)列,清空微任務(wù)隊(duì)列后友题,再回到宏任務(wù)隊(duì)列執(zhí)行下一個(gè)宏任務(wù)嗤堰。
上面案例在 Node 事件輪詢中,會(huì)將 timer
隊(duì)列清空后度宦,在輪詢下一個(gè)隊(duì)列之前執(zhí)行微任務(wù)隊(duì)列踢匣。
案例 3
setTimeout(() => {
console.log("setTimeout1");
}, 0);
setTimeout(() => {
console.log("setTimeout2");
}, 0);
Promise.resolve().then(() => {
console.log("Promise1");
});
console.log(1);
// 1
// Promise1
// setTimeout1
// setTimeout2
上面代碼的執(zhí)行過程是,先執(zhí)行棧戈抄,棧執(zhí)行時(shí)打印 1
离唬,Promise.resolve()
產(chǎn)生微任務(wù),棧執(zhí)行完畢划鸽,從棧切換到 timer
隊(duì)列之前男娄,執(zhí)行微任務(wù)隊(duì)列,再去執(zhí)行 timer
隊(duì)列漾稀。
案例 4
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
//結(jié)果1
// setImmediate1
// setTimeout2
// setTimeout1
// setImmediate2
// 結(jié)果2
// setTimeout2
// setImmediate1
// setImmediate2
// setTimeout1
setImmediate
和 setTimeout
執(zhí)行順序不固定模闲,假設(shè) check
隊(duì)列先執(zhí)行,會(huì)執(zhí)行 setImmediate
打印 setImmediate1
崭捍,將遇到的定時(shí)器放入 timer
隊(duì)列尸折,輪詢到 timer
隊(duì)列,因?yàn)樵跅V袌?zhí)行同步代碼已經(jīng)在 timer
隊(duì)列放入了一個(gè)定時(shí)器殷蛇,所以按先后順序執(zhí)行兩個(gè) setTimeout
实夹,執(zhí)行第一個(gè)定時(shí)器打印 setTimeout2
,將遇到的 setImmediate
放入 check
隊(duì)列粒梦,執(zhí)行第二個(gè)定時(shí)器打印 setTimeout1
亮航,再次輪詢到 check
隊(duì)列執(zhí)行新加入的 setImmediate
,打印 setImmediate2
匀们,產(chǎn)生結(jié)果 1
缴淋。
假設(shè) timer
隊(duì)列先執(zhí)行,會(huì)執(zhí)行 setTimeout
打印 setTimeout2
,將遇到的 setImmediate
放入 check
隊(duì)列重抖,輪詢到 check
隊(duì)列露氮,因?yàn)樵跅V袌?zhí)行同步代碼已經(jīng)在 check
隊(duì)列放入了一個(gè) setImmediate
,所以按先后順序執(zhí)行兩個(gè) setImmediate
钟沛,執(zhí)行第一個(gè) setImmediate
打印 setImmediate1
畔规,將遇到的 setTimeout
放入 timer
隊(duì)列,執(zhí)行第二個(gè) setImmediate
打印 setImmediate2
恨统,再次輪詢到 timer
隊(duì)列執(zhí)行新加入的 setTimeout
叁扫,打印 setTimeout1
,產(chǎn)生結(jié)果 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);
//結(jié)果1
// setImmediate1
// setTimeout2
// setTimeout1
// nextTick
// setImmediate2
// 結(jié)果2
// setTimeout2
// nextTick
// setImmediate1
// setImmediate2
// setTimeout1
這與上面一個(gè)案例類似陌兑,不同的是在 setTimeout
執(zhí)行的時(shí)候產(chǎn)生了一個(gè)微任務(wù) nextTick
,我們只要知道由捎,在 Node 事件輪詢中兔综,在切換隊(duì)列時(shí)要先去執(zhí)行微任務(wù)隊(duì)列,無論是 check
隊(duì)列先執(zhí)行狞玛,還是 timer
隊(duì)列先執(zhí)行软驰,都會(huì)很容易分析出上面的兩個(gè)結(jié)果。
案例 6
const fs = require("fs");
fs.readFile("./.gitignore", "utf8", function() {
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(function() {
console.log("setImmediate");
});
});
// setImmediate
// timeout
上面案例的 setTimeout
和 setImmediate
的執(zhí)行順序是固定的心肪,前面都是不固定的锭亏,這是為什么?
因?yàn)榍懊娴牟还潭ㄊ窃跅V袌?zhí)行同步代碼時(shí)就遇到了 setTimeout
和 setImmediate
硬鞍,因?yàn)闊o法判斷 Node 的準(zhǔn)備時(shí)間慧瘤,不確定準(zhǔn)備結(jié)束定時(shí)器是否到時(shí)并加入 timer
隊(duì)列。
而上面代碼明顯可以看出 Node 準(zhǔn)備結(jié)束后會(huì)直接執(zhí)行 poll
隊(duì)列進(jìn)行文件的讀取固该,在回調(diào)中將 setTimeout
和 setImmediate
分別加入 timer
隊(duì)列和 check
隊(duì)列锅减,Node 隊(duì)列的輪詢是有順序的,在 poll
隊(duì)列后應(yīng)該先切換到 check
隊(duì)列伐坏,然后再重新輪詢到 timer
隊(duì)列怔匣,所以得到上面的結(jié)果。
案例 7
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));
// nextTick
// Promise
在 Node 中有兩個(gè)微任務(wù)桦沉,Promise
的 then
方法和 process.nextTick
每瞒,從上面案例的結(jié)果我們可以看出,在微任務(wù)隊(duì)列中 process.nextTick
是優(yōu)先執(zhí)行的纯露。
上面內(nèi)容就是瀏覽器與 Node 在事件輪詢的規(guī)則剿骨,相信在讀完以后應(yīng)該已經(jīng)徹底弄清了瀏覽器的事件輪詢機(jī)制和 Node 的事件輪詢機(jī)制,并深刻的體會(huì)到了他們之間的相同和不同埠褪。