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)
如上圖所示岸售,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欣喧。
異步過程
但是在回調(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 階段有兩個功能:
- 如果發(fā)現(xiàn)計(jì)時器的時間到了入篮,就繞回到 timers 階段執(zhí)行計(jì)時器的回調(diào)。
- 然后再幌甘,執(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ì)列是空的肮帐,就會:
- 如果有 setImmediate() 任務(wù),event loop 就結(jié)束 poll 階段去往 check 階段边器。
- 如果沒有 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ī)