Event Loop事件循環(huán)振亮,GET!

JS中比較讓人頭疼的問題之一要算異步事件了鞭莽,比如我們經(jīng)常要等后臺返回?cái)?shù)據(jù)后進(jìn)行dom操作坊秸,又比如我們要設(shè)置一個定時器完成特定的要求。在這些同步與異步事件里澎怒,異步事件肯定是在同步事件之后的褒搔,但是異步事件之間又是怎么樣的一個順序呢,比如多個setTimeout事件又是怎么樣一個執(zhí)行順序喷面?這就涉及到事件循環(huán):Event Loop星瘾。

JS的單線程

雖然現(xiàn)在的JS可以用來做多方面的開發(fā),但是最初的JS是瀏覽器的專用語言惧辈,用來操作DOM琳状。所以從誕生之初,JS就被設(shè)計(jì)成單線程語言盒齿,原因是不想讓瀏覽器變得太復(fù)雜念逞,因?yàn)槎嗑€程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果县昂,對于一種網(wǎng)頁腳本語言來說肮柜,這就太復(fù)雜了。如果 JavaScript 同時有兩個線程倒彰,一個線程在網(wǎng)頁 DOM 節(jié)點(diǎn)上添加內(nèi)容,另一個線程刪除了這個節(jié)點(diǎn)莱睁,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)待讳?是不是還要有鎖機(jī)制?所以仰剿,為了避免復(fù)雜性创淡,JavaScript 一開始就是單線程,這已經(jīng)成了這門語言的核心特征南吮,將來也不會改變琳彩。

但是這種單線程機(jī)制卻制造了另一個麻煩,假如一個操作需花費(fèi)很長時間部凑,那么此時瀏覽器就會一直等待這個操作完成露乏,就會造成不好的體驗(yàn)。因此涂邀,JS的另一個事件就是異步事件瘟仿。異步事件是專門將一些事件以隊(duì)列的形式儲存到瀏覽器的任務(wù)隊(duì)列中,等同步事件執(zhí)行完后再去執(zhí)行比勉,這樣就避免了頁面堵塞劳较。

JavaScript 引擎怎么知道異步任務(wù)有沒有結(jié)果驹止,能不能進(jìn)入主線程呢?答案就是引擎在不停地檢查观蜗,一遍又一遍臊恋,只要同步任務(wù)執(zhí)行完了,引擎就會去檢查那些掛起來的異步任務(wù)墓捻,是不是可以進(jìn)入主線程了抖仅。這種循環(huán)檢查的機(jī)制,就叫做事件循環(huán)(Event Loop)毙替。

瀏覽器的事件循環(huán)

瀏覽器事件循環(huán)

如上圖所示岸售,js中的基本數(shù)據(jù)與對象都會儲存在棧內(nèi)存中,其中復(fù)雜類型數(shù)據(jù)對象會在堆內(nèi)存儲存其數(shù)據(jù)結(jié)構(gòu)厂画,棧內(nèi)存儲存的是對這個數(shù)據(jù)結(jié)構(gòu)的引用凸丸。

執(zhí)行棧

javaScript是單線程,也就是說只有一個主線程袱院,主線程有一個棧屎慢。當(dāng)JS代碼執(zhí)行時,代碼會被推入執(zhí)行棧中進(jìn)行運(yùn)行忽洛,運(yùn)行代碼的過程中腻惠,同步事件會立即執(zhí)行,其中Dom欲虚、Ajax以及SetTimeout等異步事件會注冊回調(diào)函數(shù)集灌,放入事件回調(diào)隊(duì)列中,等同步代碼執(zhí)行完之后執(zhí)行复哆。這樣一個循環(huán)便是瀏覽器的Event Loop欣喧。

執(zhí)行過程中棧的變化

異步過程

但是在回調(diào)隊(duì)列中這些事件又是怎么樣一個執(zhí)行順序呢?實(shí)際上異步隊(duì)列存在兩個隊(duì)列梯找,一個宏任務(wù)隊(duì)列唆阿,一個微任務(wù)隊(duì)列,其這就涉及到兩個概念:

  • 宏任務(wù)(MacroTask):
    包括整體代碼script锈锤,setTimeout驯鳖、setInterval、setImmediate久免、I/O浅辙、UI渲染
  • 微任務(wù)(MicroTask):
    Promise、process.nextTick妄壶、Object.observe摔握、MutationObserver

在棧內(nèi)存中代碼執(zhí)行完后,瀏覽器空閑丁寄,立即處理回調(diào)隊(duì)列氨淌,將回調(diào)隊(duì)列中的宏任務(wù)隊(duì)列中的事件推入執(zhí)行棧中執(zhí)行泊愧。

  • 首先會執(zhí)行宏任務(wù),如果宏任務(wù)中存在宏任務(wù)盛正,則會把該任務(wù)放到宏任務(wù)隊(duì)列中删咱。如果該任務(wù)里存在微任務(wù),則把微任務(wù)放在微任務(wù)隊(duì)列豪筝。
  • 在這個宏任務(wù)執(zhí)行完后痰滋,首先去看微任務(wù)隊(duì)列中是否有任務(wù),然后把微任務(wù)推到執(zhí)行棧中執(zhí)行续崖。

執(zhí)行完微任務(wù)隊(duì)列敲街,這一次循環(huán)就結(jié)束了,然后再進(jìn)行在宏任務(wù)隊(duì)列中進(jìn)行下一個宏任務(wù)严望,微任務(wù)多艇,直至回調(diào)隊(duì)列清空。

上述事件歸納后像吻,以下例說明:


代碼示例

分析:

循環(huán)1:

  • 【task隊(duì)列:script 峻黍;microtask隊(duì)列:】
    1.首先整個代碼被推到執(zhí)行棧中執(zhí)行,這是一個宏任務(wù)(整個script代碼)
    2.運(yùn)行中拨匆,同步代碼立即執(zhí)行姆涩,new Promise中的fn是立即執(zhí)行的。setTimeout被放在宏任務(wù)隊(duì)列中惭每,promise1骨饿、promise2被放在微任務(wù)隊(duì)列中。
  • 【task隊(duì)列:setTimeout 台腥;microtask隊(duì)列:promise1样刷、promise2】
    3.宏任務(wù)script執(zhí)行完后,執(zhí)行微任務(wù)隊(duì)列览爵,取出microtask隊(duì)列,推入執(zhí)行棧執(zhí)行镇饮,第一次循環(huán)到此結(jié)束蜓竹。

循環(huán)2:

  • 【task隊(duì)列:setTimeout ;microtask隊(duì)列:】
    4.取出宏任務(wù)中的setTimeout推入執(zhí)行棧執(zhí)行储藐,如果有微任務(wù)則俱济,則被放在微任務(wù)隊(duì)列(這里沒有)。
    5.宏任務(wù)執(zhí)行完钙勃,去微任務(wù)隊(duì)列執(zhí)行(微任務(wù)隊(duì)列為空)蛛碌。
  • 【task隊(duì)列:;microtask隊(duì)列:】
    6.宏任務(wù)隊(duì)列為空辖源,循環(huán)至此結(jié)束蔚携。

Nodejs 事件循環(huán)

nodejs中的事件循環(huán)跟瀏覽器不一樣希太,瀏覽器的循環(huán)是遵循ES標(biāo)準(zhǔn)里的,nodejs里的循環(huán)是通過LIBUV庫實(shí)現(xiàn)的酝蜒。

當(dāng) Node.js 啟動時誊辉,會做這幾件事

  • 初始化 event loop
  • 開始執(zhí)行腳本(或者進(jìn)入 REPL,本文不涉及 REPL)亡脑。這些腳本有可能會調(diào)用一些異步 API堕澄、設(shè)定計(jì)時器或者調(diào)用 process.nextTick()
  • 開始處理 event loop

nodejs的Event Loop 一共有6個階段:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

其中我們主要需要關(guān)注的是timers、poll霉咨、check階段:

  • timers 階段:這個階段執(zhí)行 setTimeout 和 setInterval 的回調(diào)函數(shù)蛙紫。
  • I/O callbacks 階段:不在 timers 階段、close callbacks 階段和 check 階段這三個階段執(zhí)行的回調(diào)途戒,都由此階段負(fù)責(zé)坑傅,這幾乎包含了所有回調(diào)函數(shù)。
  • idle, prepare 階段(譯注:看起來是兩個階段棺滞,不過這不重要):event loop 內(nèi)部使用的階段(譯注:我們不用關(guān)心這個階段)
  • poll 階段:獲取新的 I/O 事件裁蚁。在某些場景下 Node.js 會阻塞在這個階段。
  • check 階段:執(zhí)行 setImmediate() 的回調(diào)函數(shù)继准。
  • close callbacks 階段:執(zhí)行關(guān)閉事件的回調(diào)函數(shù)枉证,如 socket.on('close', fn) 里的 fn。

timers階段

計(jì)時器實(shí)際上是在指定多久以后可以執(zhí)行某個回調(diào)函數(shù)移必,而不是指定某個函數(shù)的確切執(zhí)行時間室谚。當(dāng)指定的時間達(dá)到后,計(jì)時器的回調(diào)函數(shù)會盡早被執(zhí)行崔泵。如果操作系統(tǒng)很忙秒赤,或者 Node.js 正在執(zhí)行一個耗時的函數(shù),那么計(jì)時器的回調(diào)函數(shù)就會被推遲執(zhí)行憎瘸。

poll 階段(輪詢階段)

poll 階段有兩個功能:

  1. 如果發(fā)現(xiàn)計(jì)時器的時間到了入篮,就繞回到 timers 階段執(zhí)行計(jì)時器的回調(diào)。
  2. 然后再幌甘,執(zhí)行 poll 隊(duì)列里的回調(diào)潮售。

當(dāng) event loop 進(jìn)入 poll 階段,如果發(fā)現(xiàn)沒有計(jì)時器锅风,就會:

  • 如果 poll 隊(duì)列不是空的酥诽,event loop 就會依次執(zhí)行隊(duì)列里的回調(diào)函數(shù),直到隊(duì)列被清空或者到達(dá) poll 階段的時間上限皱埠。
  • 如果 poll 隊(duì)列是空的肮帐,就會:
    1. 如果有 setImmediate() 任務(wù),event loop 就結(jié)束 poll 階段去往 check 階段边器。
    2. 如果沒有 setImmediate() 任務(wù)训枢,event loop 就會等待新的回調(diào)函數(shù)進(jìn)入 poll 隊(duì)列托修,并立即執(zhí)行它。

一旦 poll 隊(duì)列為空肮砾,event loop 就會檢查計(jì)時器有沒有到期诀黍,如果有計(jì)時器到期了,event loop 就會回到 timers 階段執(zhí)行計(jì)時器的回調(diào)仗处。

check 階段

這個階段允許開發(fā)者在 poll 階段結(jié)束后立即執(zhí)行一些函數(shù)眯勾。如果 poll 階段空閑了,同時存在 setImmediate() 任務(wù)婆誓,event loop 就會進(jìn)入 check 階段吃环,執(zhí)行setImmediate() 回調(diào)。

Event Loop 大體流程

每一個階段都有一個隊(duì)列洋幻,我們只關(guān)注 timers郁轻、poll、check階段來分析一下文留,我們在用命令行運(yùn)行 node server.js 命令時好唯,發(fā)生了什么:
1、Node.js啟動,初始化 Event Loop
2、運(yùn)行server.js腳本內(nèi)容
3锰霜、開始運(yùn)行Event Loop
4踩官、timers階段看腳本里是否設(shè)置定時器setTimeout稼虎,比如一個4ms延遲與一個100ms延遲的定時器,把它放到timers隊(duì)列中,進(jìn)入下一步,I/O callbacks 階段杨名,idle, prepare 階段,這兩個階段都不會停留猖毫。
5台谍、進(jìn)入poll(輪詢)階段,首先它會查看定時器時間是否到了吁断,比如4ms到了典唇,他就進(jìn)入下一階段check階段、close callbacks 階段胯府,然后回到timers階段執(zhí)行設(shè)置的4ms回調(diào)函數(shù),接著繼續(xù)第4步到第5步恨胚。4ms沒到骂因,則停留在這一階段,處理poll隊(duì)列里的任務(wù)赃泡,直到4ms到寒波、100ms到乘盼,然后循環(huán)回到timers階段執(zhí)行回調(diào)。

這里有一個問題:當(dāng)poll階段在處理任務(wù)1時俄烁,比如這個任務(wù)1要花費(fèi)100ms绸栅,在這100ms期間,setTimeout定時器到了页屠,則它的回調(diào)會等poll處理玩任務(wù)1后立即循環(huán)進(jìn)入timers階段執(zhí)行

6粹胯、從poll階段進(jìn)入check階段時,主要是看是否有setImmediate() 任務(wù)辰企,如果有則立即執(zhí)行风纠,然后再進(jìn)入close callbacks 階段,進(jìn)行循環(huán)牢贸,進(jìn)入timers階段竹观。

setImmediate() vs setTimeout()

setImmediate 和 setTimeout 很相似,但是其回調(diào)函數(shù)的調(diào)用時機(jī)卻不一樣潜索。

setImmediate() 的作用是在當(dāng)前 poll 階段結(jié)束后調(diào)用一個函數(shù)臭增。 setTimeout() 的作用是在一段時間后調(diào)用一個函數(shù)。一般來說 setImmediate 會先于 setTimeout 執(zhí)行竹习,但是第一次啟動的時候不一樣誊抛,這兩者的回調(diào)的執(zhí)行順序取決于 setTimeout 和 setImmediate 被調(diào)用時的環(huán)境。
例如:

setTimeout(()=>{
    console.log('setTiomeout')
},0)

setInmediate(()=>{
    console.log('setInmediate')
})

為什么會發(fā)生這種情況呢由驹?因?yàn)槲覀儐觧ode.js時, node會做三件事, 初始化event loop芍锚,運(yùn)行腳本,開始event loop。運(yùn)行腳本與開始event loop這兩件事不是同時執(zhí)行的蔓榄,它兩中間間隔多少并不清楚并炮,這跟環(huán)境性能有關(guān)。然后要注意的一點(diǎn)甥郑,setTimeout的延遲時間最小為4ms逃魄,所以這里的0相當(dāng)于4。

  • 可能兩者間隔5ms澜搅,當(dāng)進(jìn)入timers階段的時候伍俘,node發(fā)現(xiàn),4ms已經(jīng)過了勉躺,立即執(zhí)行setTimeout定時器回調(diào)癌瘾,然后執(zhí)行setImmediate。
  • 也可能兩者間隔3ms饵溅,當(dāng)進(jìn)入timers階段的時候妨退,node發(fā)現(xiàn),4ms還沒過,就進(jìn)入下一階段咬荷,一直到checked冠句,執(zhí)行setImmediate,然后等到4ms時再執(zhí)行setTimeout幸乒。

process.nextTick()

從技術(shù)上來講 process.nextTick() 并不是 event loop 的一部分懦底。實(shí)際上,event loop 再次進(jìn)入循環(huán)前罕扎,會去先執(zhí)行process.nextTick()聚唐。

setTimeout(()=>{
    console.log('setTiomeout')
},0)

setInmediate(()=>{
    console.log('setInmediate')
})

proces.nextTick(()=>{
    console.log('nextTick')
})

上述代碼中nextTick先于其它兩個執(zhí)行,Vue中有Vue.nextTick()方法就是類似的思想壳影。

注:
本篇文章參考:
Event Loop拱层、計(jì)時器、nextTick
這一次宴咧,徹底弄懂 JavaScript 執(zhí)行機(jī)制
從event loop規(guī)范探究javaScript異步及瀏覽器更新渲染時機(jī)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末根灯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掺栅,更是在濱河造成了極大的恐慌烙肺,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氧卧,死亡現(xiàn)場離奇詭異桃笙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沙绝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門搏明,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闪檬,你說我怎么就攤上這事星著。” “怎么了粗悯?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵虚循,是天一觀的道長。 經(jīng)常有香客問我样傍,道長横缔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任衫哥,我火速辦了婚禮茎刚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撤逢。我一直安慰自己斗蒋,他們只是感情好捌斧,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泉沾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妇押。 梳的紋絲不亂的頭發(fā)上跷究,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音敲霍,去河邊找鬼俊马。 笑死,一個胖子當(dāng)著我的面吹牛肩杈,可吹牛的內(nèi)容都是我干的柴我。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼扩然,長吁一口氣:“原來是場噩夢啊……” “哼艘儒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起夫偶,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤界睁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后兵拢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翻斟,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年说铃,在試婚紗的時候發(fā)現(xiàn)自己被綠了访惜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡腻扇,死狀恐怖债热,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情衙解,我是刑警寧澤阳柔,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站蚓峦,受9級特大地震影響舌剂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜暑椰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一霍转、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧一汽,春花似錦避消、人聲如沸低滩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恕沫。三九已至,卻和暖如春纱意,著一層夾襖步出監(jiān)牢的瞬間婶溯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工偷霉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迄委,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓类少,卻偏偏與公主長得像叙身,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子硫狞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359

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