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

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

JS的單線程

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

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

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

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

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

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

執(zhí)行棧

javaScript是單線程,也就是說(shuō)只有一個(gè)主線程,主線程有一個(gè)棧。當(dāng)JS代碼執(zhí)行時(shí),代碼會(huì)被推入執(zhí)行棧中進(jìn)行運(yùn)行,運(yùn)行代碼的過(guò)程中,同步事件會(huì)立即執(zhí)行,其中Dom、Ajax以及SetTimeout等異步事件會(huì)注冊(cè)回調(diào)函數(shù),放入事件回調(diào)隊(duì)列中,等同步代碼執(zhí)行完之后執(zhí)行。這樣一個(gè)循環(huán)便是瀏覽器的Event Loop。

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

異步過(guò)程

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

  • 宏任務(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í)行。

  • 首先會(huì)執(zhí)行宏任務(wù),如果宏任務(wù)中存在宏任務(wù),則會(huì)把該任務(wù)放到宏任務(wù)隊(duì)列中。如果該任務(wù)里存在微任務(wù),則把微任務(wù)放在微任務(wù)隊(duì)列。
  • 在這個(gè)宏任務(wù)執(zhí)行完后,首先去看微任務(wù)隊(duì)列中是否有任務(wù),然后把微任務(wù)推到執(zhí)行棧中執(zhí)行。

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

上述事件歸納后,以下例說(shuō)明:


代碼示例

分析:

循環(huán)1:

  • 【task隊(duì)列:script ;microtask隊(duì)列:】
    1.首先整個(gè)代碼被推到執(zhí)行棧中執(zhí)行,這是一個(gè)宏任務(wù)(整個(gè)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ì)列(這里沒(méi)有)。
    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)是通過(guò)LIBUV庫(kù)實(shí)現(xiàn)的。

當(dāng) Node.js 啟動(dòng)時(shí),會(huì)做這幾件事

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

nodejs的Event Loop 一共有6個(gè)階段:

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

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

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

timers階段

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

poll 階段(輪詢階段)

poll 階段有兩個(gè)功能:

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

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

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

一旦 poll 隊(duì)列為空,event loop 就會(huì)檢查計(jì)時(shí)器有沒(méi)有到期,如果有計(jì)時(shí)器到期了,event loop 就會(huì)回到 timers 階段執(zhí)行計(jì)時(shí)器的回調(diào)。

check 階段

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

Event Loop 大體流程

每一個(gè)階段都有一個(gè)隊(duì)列,我們只關(guān)注 timers、poll、check階段來(lái)分析一下,我們?cè)谟妹钚羞\(yùn)行 node server.js 命令時(shí),發(fā)生了什么:
1、Node.js啟動(dòng),初始化 Event Loop
2、運(yùn)行server.js腳本內(nèi)容
3、開(kāi)始運(yùn)行Event Loop
4、timers階段看腳本里是否設(shè)置定時(shí)器setTimeout,比如一個(gè)4ms延遲與一個(gè)100ms延遲的定時(shí)器,把它放到timers隊(duì)列中,進(jìn)入下一步,I/O callbacks 階段,idle, prepare 階段,這兩個(gè)階段都不會(huì)停留。
5、進(jìn)入poll(輪詢)階段,首先它會(huì)查看定時(shí)器時(shí)間是否到了,比如4ms到了,他就進(jìn)入下一階段check階段、close callbacks 階段,然后回到timers階段執(zhí)行設(shè)置的4ms回調(diào)函數(shù),接著繼續(xù)第4步到第5步。4ms沒(méi)到,則停留在這一階段,處理poll隊(duì)列里的任務(wù),直到4ms到、100ms到,然后循環(huán)回到timers階段執(zhí)行回調(diào)。

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

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

setImmediate() vs setTimeout()

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

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

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

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

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

  • 可能兩者間隔5ms,當(dāng)進(jìn)入timers階段的時(shí)候,node發(fā)現(xiàn),4ms已經(jīng)過(guò)了,立即執(zhí)行setTimeout定時(shí)器回調(diào),然后執(zhí)行setImmediate。
  • 也可能兩者間隔3ms,當(dāng)進(jìn)入timers階段的時(shí)候,node發(fā)現(xiàn),4ms還沒(méi)過(guò),就進(jìn)入下一階段,一直到checked,執(zhí)行setImmediate,然后等到4ms時(shí)再執(zhí)行setTimeout。

process.nextTick()

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

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

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

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容