淺析 event-loop 事件輪詢


原文出自: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茉盏、setIntervalsetImmediate枢冤、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ù)祟峦,setImmediatesetTimeout 延遲時(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)情況下 setTimeoutsetImmediate 是不知道哪一個(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

setImmediatesetTimeout 執(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

上面案例的 setTimeoutsetImmediate 的執(zhí)行順序是固定的心肪,前面都是不固定的锭亏,這是為什么?

因?yàn)榍懊娴牟还潭ㄊ窃跅V袌?zhí)行同步代碼時(shí)就遇到了 setTimeoutsetImmediate硬鞍,因?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)中將 setTimeoutsetImmediate 分別加入 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ù)桦沉,Promisethen 方法和 process.nextTick每瞒,從上面案例的結(jié)果我們可以看出,在微任務(wù)隊(duì)列中 process.nextTick 是優(yōu)先執(zhí)行的纯露。

上面內(nèi)容就是瀏覽器與 Node 在事件輪詢的規(guī)則剿骨,相信在讀完以后應(yīng)該已經(jīng)徹底弄清了瀏覽器的事件輪詢機(jī)制和 Node 的事件輪詢機(jī)制,并深刻的體會(huì)到了他們之間的相同和不同埠褪。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末浓利,一起剝皮案震驚了整個(gè)濱河市挤庇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荞膘,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玉工,死亡現(xiàn)場(chǎng)離奇詭異羽资,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)遵班,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門屠升,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狭郑,你說我怎么就攤上這事腹暖。” “怎么了翰萨?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵脏答,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我亩鬼,道長(zhǎng)殖告,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任雳锋,我火速辦了婚禮黄绩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘玷过。我一直安慰自己爽丹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布辛蚊。 她就那樣靜靜地躺著粤蝎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪袋马。 梳的紋絲不亂的頭發(fā)上诽里,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音飞蛹,去河邊找鬼谤狡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卧檐,可吹牛的內(nèi)容都是我干的墓懂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼霉囚,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼捕仔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤榜跌,失蹤者是張志新(化名)和其女友劉穎闪唆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钓葫,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悄蕾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了础浮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帆调。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖豆同,靈堂內(nèi)的尸體忽然破棺而出番刊,到底是詐尸還是另有隱情,我是刑警寧澤影锈,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布芹务,位于F島的核電站鸭廷,受9級(jí)特大地震影響锄禽,放射性物質(zhì)發(fā)生泄漏靴姿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一佛吓、第九天 我趴在偏房一處隱蔽的房頂上張望宵晚。 院中可真熱鬧,春花似錦维雇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽津滞。三九已至铝侵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間触徐,已是汗流浹背咪鲜。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工撞鹉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留颖侄,地道東北人享郊。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像炊琉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子温自,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容