自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

瀏覽器和 Node.js 的 EventLoop 事件循環(huán)機(jī)制知多少?

系統(tǒng) 瀏覽器
本篇文章談了EventLoop在瀏覽器和Node.js中的區(qū)別,EventLoop本身不是什么比較復(fù)雜的概念,只是我們需要根據(jù)JS的不同運(yùn)行平臺(tái),理解它們之間的相同和差異。

[[439223]]

本文轉(zhuǎn)載自微信公眾號(hào)「前端萬(wàn)有引力」,作者一川 。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端萬(wàn)有引力公眾號(hào)。

1.寫(xiě)在前面

無(wú)論是瀏覽器端還是服務(wù)端Node.js,都在使用EventLoop事件循環(huán)機(jī)制,都是基于Javascript語(yǔ)言的單線(xiàn)程和非阻塞IO的特點(diǎn)。在EventLoop事件隊(duì)列中有宏任務(wù)和微任務(wù)隊(duì)列,分析宏任務(wù)和微任務(wù)的運(yùn)行機(jī)制,有助于我們理解代碼在瀏覽器中的執(zhí)行邏輯。

那么,我們得思考幾個(gè)問(wèn)題:

  • 瀏覽器的EventLoop發(fā)揮著什么作用?
  • Node.js服務(wù)端的EventLoop發(fā)揮著什么作用?
  • 宏任務(wù)和微任務(wù)分別有哪些方法?
  • 宏任務(wù)和微任務(wù)互相嵌套,執(zhí)行順序是什么樣的?
  • Node.js中的Process.nextick和其它微任務(wù)方法在一起的時(shí)候執(zhí)行順序是什么?
  • Vue也有個(gè)nextick,它的邏輯又是什么樣的呢?

2.瀏覽器的EventLoop

EventLoop是Javascript引擎異步編程需要著重關(guān)注的知識(shí)點(diǎn),也是在學(xué)習(xí)JS底層原理所必須學(xué)習(xí)的關(guān)鍵。我們知道JS在單線(xiàn)程上執(zhí)行所有的操作,雖然是單線(xiàn)程的,但是總是能夠高效地解決問(wèn)題,并且會(huì)給我們帶來(lái)一種『多線(xiàn)程』的錯(cuò)覺(jué)。這其實(shí)是通過(guò)一些高效合理的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這種效果的。

調(diào)用棧(Call Stack)

調(diào)用堆棧:負(fù)責(zé)追蹤所有要執(zhí)行的代碼。每當(dāng)調(diào)用堆棧中的函數(shù)執(zhí)行完畢時(shí),就會(huì)從棧中彈出此函數(shù),如果有代碼需要輸入就會(huì)執(zhí)行PUSH操作。

事件隊(duì)列(Event Queue)

事件隊(duì)列:負(fù)責(zé)將新的函數(shù)發(fā)送到隊(duì)列中進(jìn)行處理。事件執(zhí)行隊(duì)列符合數(shù)據(jù)結(jié)構(gòu)中的隊(duì)列,先進(jìn)先出的特性,當(dāng)先進(jìn)入的事件先執(zhí)行,執(zhí)行完畢先彈出。

 

每當(dāng)調(diào)用事件隊(duì)列(Event Queue)中的異步函數(shù)時(shí),都會(huì)將其發(fā)送到瀏覽器API。根據(jù)調(diào)用棧收到的命令,API開(kāi)始自己的單線(xiàn)程操作。

比如,在事件執(zhí)行隊(duì)列操作setTimeout事件時(shí),會(huì)現(xiàn)將其發(fā)送到瀏覽器對(duì)應(yīng)的API,該API會(huì)一直等到約定的時(shí)間將其送回調(diào)用棧進(jìn)行處理。即,它將操作發(fā)送到事件隊(duì)列中,這樣就形成了一個(gè)循環(huán)系統(tǒng),用于Javascript中進(jìn)行異步操作。

Javascript語(yǔ)言本身是單線(xiàn)程的,而瀏覽器的API充當(dāng)獨(dú)立的線(xiàn)程,事件循環(huán)促進(jìn)了這一過(guò)程,它會(huì)不斷檢查調(diào)用棧的代碼是否為空。如果為空,就從事件執(zhí)行隊(duì)列中添加到調(diào)用棧中;如果不為空,則優(yōu)先執(zhí)行當(dāng)前調(diào)用棧中的代碼。

在EventLoop中,每次循環(huán)稱(chēng)為一次tick。主要順序是:

  • 執(zhí)行棧選擇最先進(jìn)入隊(duì)列的宏任務(wù),執(zhí)行其同步代碼直到結(jié)束
  • 檢查是否有微任務(wù),如果有則執(zhí)行知道微任務(wù)隊(duì)列為空
  • 如果是在瀏覽器端,那么基本要渲染頁(yè)面
  • 開(kāi)始下一輪的循環(huán)tick,執(zhí)行宏任務(wù)中的一些異步代碼,如:setTimeout

注意:最先進(jìn)行調(diào)用棧的宏任務(wù),一般情況下都是最后返回執(zhí)行的結(jié)果。

事實(shí)上,EventLoop通過(guò)內(nèi)部?jī)蓚€(gè)隊(duì)列來(lái)實(shí)現(xiàn)Event Queue放進(jìn)來(lái)的異步任務(wù)。以setTimeout為代表的任務(wù)稱(chēng)為宏任務(wù),放在宏任務(wù)隊(duì)列(Macrotask Queue)中;以Promise為代表的任務(wù)稱(chēng)為微任務(wù),放在微任務(wù)隊(duì)列(Microtask Queue)中。

主要的宏任務(wù)和微任務(wù)有:

  • 宏任務(wù)(Macrotask Queue):
    • script整體代碼
    • setTimeout、setInterval
    • setimmediate
    • I/O (網(wǎng)絡(luò)請(qǐng)求完成、文件讀寫(xiě)完畢事件)
    • UI 渲染(解析DOM、計(jì)算布局、繪制)
    • EventListner事件監(jiān)聽(tīng)(鼠標(biāo)點(diǎn)擊、滾動(dòng)頁(yè)面、放大縮小等)
  • 微任務(wù)(Microtask Queue):
  • process.nextTick
  • Promise
  • Object.observe
  • MutationObserver

宏任務(wù)

頁(yè)面進(jìn)程中引入了消息隊(duì)列和事件循環(huán)機(jī)制,我們把這些消息隊(duì)列中的任務(wù)稱(chēng)為宏任務(wù)。JS代碼中不能準(zhǔn)確掌控任務(wù)要添加到隊(duì)列中的位置,控制不了任務(wù)在消息隊(duì)列中的位置,所以很難控制開(kāi)始執(zhí)行任務(wù)的時(shí)間。例如:

  1. function func2(){ 
  2.  console.log(2); 
  3. function func(){ 
  4.  console.log(1); 
  5.   setTimeout(func2,0); 
  6.  
  7. setTimeout(func,0); 

你以為上面的代碼會(huì)一次打印1和2嗎,并不是。因?yàn)樵贘S事件循環(huán)機(jī)制中,當(dāng)執(zhí)行setTimeout時(shí)會(huì)將事件進(jìn)行掛起,執(zhí)行一些其它的系統(tǒng)任務(wù),當(dāng)其他的執(zhí)行完畢之后才會(huì)執(zhí)行,因此執(zhí)行時(shí)間間隔是不可控。

微任務(wù)

微任務(wù)是一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行完畢后、當(dāng)前宏任務(wù)結(jié)束前。JS執(zhí)行一段腳本時(shí),v8引擎會(huì)為其創(chuàng)建一個(gè)全局執(zhí)行上下文,同時(shí)v8引擎會(huì)在其內(nèi)部創(chuàng)建一個(gè)微任務(wù)隊(duì)列,這個(gè)微任務(wù)隊(duì)列就是用來(lái)存放微任務(wù)的。

那么微任務(wù)是如何產(chǎn)生的呢?

  • 使用MutationObserver監(jiān)控某個(gè)DOM節(jié)點(diǎn),或者為這個(gè)節(jié)點(diǎn)添加、刪除部分子節(jié)點(diǎn),當(dāng)DOM節(jié)點(diǎn)發(fā)生變化時(shí),就會(huì)產(chǎn)生DOM變化記錄的微任務(wù)。
  • 使用Promise,當(dāng)調(diào)用Promise.resolve()或者Promise.reject()時(shí),也會(huì)產(chǎn)生微任務(wù)。

通過(guò)DOM節(jié)點(diǎn)變化產(chǎn)生的微任務(wù)或使用Promise產(chǎn)生的微任務(wù)會(huì)被JS引擎按照順序保存到微任務(wù)隊(duì)列中。

MutationObserver是用來(lái)監(jiān)聽(tīng)DOM變化的一套方法,雖然監(jiān)聽(tīng)DOM需求比較頻繁,不過(guò)早期頁(yè)面并沒(méi)有提供對(duì)監(jiān)聽(tīng)的支持,唯一能做的就是進(jìn)行輪詢(xún)檢測(cè)。如果設(shè)置時(shí)間間隔過(guò)長(zhǎng),DOM變化響應(yīng)不夠及時(shí);如果時(shí)間間隔過(guò)短,又會(huì)浪費(fèi)很多無(wú)用的工作量去檢查DOM。從DOM4開(kāi)始,W3C推出了MutationObserver可以用于監(jiān)視DOM變化,包括屬性的變更、節(jié)點(diǎn)的增加、內(nèi)容的改變等。在每次DOM節(jié)點(diǎn)發(fā)生變化的時(shí)候,渲染引擎將變化記錄封裝成微任務(wù),并將微任務(wù)添加到當(dāng)前的微任務(wù)隊(duì)列中。

MutationObserver采用了"異步+微任務(wù)"策略,通過(guò)異步操作解決了同步操作的性能問(wèn)題,通過(guò)微任務(wù)解決了實(shí)時(shí)性問(wèn)題。

JS引擎在準(zhǔn)備退出全局執(zhí)行上下文并清空調(diào)用棧的時(shí)候,JS引擎會(huì)檢查全局執(zhí)行上下文中的微任務(wù)隊(duì)列,然后按照順序執(zhí)行隊(duì)列中的微任務(wù)。在執(zhí)行微任務(wù)過(guò)程中產(chǎn)生的新的微任務(wù),并不會(huì)推遲到下一個(gè)循環(huán)中執(zhí)行,而是在當(dāng)前的循環(huán)中繼續(xù)執(zhí)行。

微任務(wù)和宏任務(wù)是綁定的,每個(gè)宏任務(wù)執(zhí)行時(shí),會(huì)創(chuàng)建自己的微任務(wù)隊(duì)列。微任務(wù)的執(zhí)行時(shí)長(zhǎng)會(huì)影響當(dāng)前宏任務(wù)的時(shí)長(zhǎng)。在一個(gè)宏任務(wù)中,分別創(chuàng)建一個(gè)用于回調(diào)的宏任務(wù)和微任務(wù),無(wú)論在什么情況下,微任務(wù)都早于宏任務(wù)執(zhí)行。

瀏覽器EventLoop的原理是:

  • JS引擎首先從宏任務(wù)隊(duì)列中取出第一個(gè)任務(wù)
  • 執(zhí)行完畢后,再將微任務(wù)中的所有任務(wù)取出,按照順序依次全部執(zhí)行;如果在此過(guò)程中產(chǎn)生了新的微任務(wù),也需要依次全部執(zhí)行
  • 然后再?gòu)暮耆蝿?wù)隊(duì)列中取出下一個(gè),執(zhí)行完畢后,再將此宏任務(wù)事件中的微任務(wù)從微任務(wù)隊(duì)列中全部取出依次執(zhí)行,循環(huán)往復(fù),知道宏任務(wù)和微任務(wù)隊(duì)列中的事件全部執(zhí)行完畢

注意:一次EventLoop循環(huán)會(huì)處理一個(gè)宏任務(wù)和所有此處循環(huán)中產(chǎn)生的微任務(wù)。

3.Node.js的EventLoop

Node.js官網(wǎng)的定義是:當(dāng) Node.js 啟動(dòng)后,它會(huì)初始化事件循環(huán),處理已提供的輸入腳本(或丟入 REPL,本文不涉及到),它可能會(huì)調(diào)用一些異步的 API、調(diào)度定時(shí)器,或者調(diào)用 process.nextTick(),然后開(kāi)始處理事件循環(huán)。

Node.js中的事件循環(huán)機(jī)制

上圖是Node.js的EventLoop流程圖,我們依次進(jìn)行分析得到:

  • Timers階段:執(zhí)行的是setTimeout和setInterval
  • I/O回調(diào)階段:執(zhí)行系統(tǒng)級(jí)別的回調(diào)函數(shù),比如TCP執(zhí)行失敗的回調(diào)函數(shù)
  • Idle、Prepare階段:Node內(nèi)部的閑置和預(yù)備階段
  • Poll階段:檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)(幾乎所有情況下,除了關(guān)閉的回調(diào)函數(shù),那些由計(jì)時(shí)器和 setImmediate() 調(diào)度的之外),其余情況 node 將在適當(dāng)?shù)臅r(shí)候在此阻塞。
  • Check階段:setImmediate() 回調(diào)函數(shù)在這里執(zhí)行。
  • Close回調(diào)階段:一些關(guān)閉的回調(diào)函數(shù),如:socket.on('close', ...)。

瀏覽器端任務(wù)隊(duì)列每輪事件循環(huán)僅出隊(duì)一個(gè)回調(diào)函數(shù),接著去執(zhí)行微任務(wù)隊(duì)列。而Node.js端只要輪到執(zhí)行某個(gè)宏任務(wù)隊(duì)列,就會(huì)執(zhí)行完隊(duì)列中的所有當(dāng)前任務(wù),但是每次輪詢(xún)新添加到隊(duì)尾的任務(wù)則會(huì)等待下一次輪詢(xún)才會(huì)執(zhí)行。

4.Process.nextTick()

  1. process.nextTick(callback,可選參數(shù)args); 

Process.nextTick會(huì)將callback添加到"nextTick queue"隊(duì)列中,nextick queue會(huì)在當(dāng)前Javascript stack執(zhí)行完畢后,下一次EventLoop開(kāi)始執(zhí)行前按照FIFO出隊(duì)。如果遞歸調(diào)用Process.nextTick可能會(huì)導(dǎo)致一個(gè)無(wú)限循環(huán),需要去適當(dāng)?shù)臅r(shí)機(jī)終止遞歸。

Process.nextTick其實(shí)是微任務(wù),同時(shí)也是異步API的一部分,但是從技術(shù)而言Process.nextTick并不是事件循環(huán)(EventLoop)的一部分。如果任何時(shí)刻在給定的階段調(diào)用Process.nextick,則所有被傳入Process.nextTick的回調(diào),將會(huì)在事件循環(huán)繼續(xù)往下執(zhí)行前被執(zhí)行,這可能導(dǎo)致事件循環(huán)永遠(yuǎn)無(wú)法到達(dá)輪詢(xún)階段。

為什么Process.nextTick這樣的API會(huì)被允許存在于Nodejs中呢?部分原因是因?yàn)樵O(shè)計(jì)理念,在nodejs中api總是異步的,即使那些不需要異步的地方。

  1. function apiCall(args,callback){ 
  2.   if(typeof args !== "string"){ 
  3.    return process.nextTick(callback,new TypeError("atgument should be string")); 
  4.   } 

我們可以看到上面的代碼,可以將一個(gè)錯(cuò)誤傳遞給用戶(hù),但這只允許在用戶(hù)代碼被執(zhí)行完畢后執(zhí)行。使用process.nextTick可以保證apiCall()的回調(diào)總是在用戶(hù)代碼被執(zhí)行后,且在事件循環(huán)繼續(xù)工作前被執(zhí)行。

那么Vue中nextTick又是做啥的呢?

vue異步執(zhí)行DOM的更新,當(dāng)數(shù)據(jù)發(fā)生變化時(shí),vue會(huì)開(kāi)啟一個(gè)隊(duì)列,用于緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)改變的情況。如果同一個(gè)watcher被多次觸發(fā),只會(huì)被推入隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù),對(duì)于避免不必要的計(jì)算和DOM操作上非常重要。然后在下一個(gè)事件循環(huán)tick中。例如:當(dāng)你設(shè)置vm.someData = "yichuan",該組件不會(huì)立即執(zhí)行重新渲染。當(dāng)刷新隊(duì)列是,組件會(huì)在事件循環(huán)隊(duì)列清空時(shí)的下一個(gè)"tick"更新。

process.nextTick的執(zhí)行順序是:每一次EventLoop執(zhí)行前,如果有多個(gè)process.nextTick,會(huì)影響下一次時(shí)間循環(huán)的執(zhí)行時(shí)間

Vue:nextick方法中每次數(shù)據(jù)更新將會(huì)在下一次作用到視圖更新

5.EventLoop對(duì)渲染的影響

requestIdlecallback和requestAnimationFrame這兩個(gè)方法不屬于JS的原生方法,而是瀏覽器宿主環(huán)境提供的方法。瀏覽器作為一個(gè)復(fù)雜的應(yīng)用是多線(xiàn)程工作的,JS線(xiàn)程可以讀取并且修改DOM,而渲染線(xiàn)程也需要讀取DOM,這是一個(gè)典型的多線(xiàn)程競(jìng)爭(zhēng)資源的問(wèn)題。所以瀏覽器把這兩個(gè)線(xiàn)程設(shè)計(jì)為互斥的,即同時(shí)只能有一個(gè)線(xiàn)程進(jìn)行運(yùn)行。

JS線(xiàn)程和渲染線(xiàn)程本來(lái)是互斥的,但是requestAnimationFrame卻讓這對(duì)水火不相容的線(xiàn)程建立起了聯(lián)系,即把EventLoop和渲染建立起了聯(lián)系。通過(guò)調(diào)用requestAnimationFrame()方法,我們可以在瀏覽器下次渲染之前執(zhí)行回調(diào)函數(shù),那么下次渲染具體在什么時(shí)間節(jié)點(diǎn)呢?渲染和EventLoop又有著什么聯(lián)系呢?

簡(jiǎn)而言之,就是在每次EventLoop結(jié)束前,判斷當(dāng)前是否有渲染時(shí)機(jī)即重新渲染,而渲染時(shí)機(jī)是有屏幕限制的,瀏覽器的刷新幀率是60Hz,即1s內(nèi)刷新了60次。此時(shí)瀏覽器的渲染時(shí)間就沒(méi)必要小于16.6ms,因?yàn)殇秩玖似聊灰膊粫?huì)進(jìn)行展示,

當(dāng)然瀏覽器也不能保證每16.6ms會(huì)渲染一次。此外,瀏覽器渲染還會(huì)收到處理器的性能以及js執(zhí)行效率等因素的影響。

requestAnimationFrame保證在瀏覽器下次渲染前一定會(huì)被調(diào)用,實(shí)際上我們完全可以將其當(dāng)成一個(gè)高級(jí)版的setInterval定時(shí)器。它們都是每隔一段時(shí)間執(zhí)行一次回調(diào)函數(shù),只不過(guò)requestAnimationFrame的時(shí)間間隔是瀏覽器不斷進(jìn)行調(diào)整的,而setInterval的時(shí)間間隔是用戶(hù)進(jìn)行指定的。因此,requestAnimationFrame更適合用于做每一幀動(dòng)畫(huà)的修改效果。

requestAnimationFrame不是EventLoop中的宏任務(wù),或者說(shuō)它并不在EventLoop的生命周期中,只是瀏覽器又開(kāi)發(fā)的一個(gè)在渲染前發(fā)生的新hook。此時(shí),我們對(duì)于微任務(wù)的認(rèn)知也需要進(jìn)行更新,在執(zhí)行requestAnimationFrame的callback函數(shù)時(shí),也有可能產(chǎn)生微任務(wù)會(huì)放在requestAnimationFrame處理完畢之后執(zhí)行。因此,微任務(wù)并不像之前描述的在每一次EventLoop后執(zhí)行處理,而是在JS函數(shù)調(diào)用棧清空后處理。

在EventLoop中并沒(méi)有什么任務(wù)需要處理時(shí),瀏覽器可能處于空閑狀態(tài),在這段空閑時(shí)間可以被requestIdlecallback利用,用于執(zhí)行一些優(yōu)先不高、不必立即執(zhí)行的任務(wù),如圖所示:

同時(shí),為了避免瀏覽器一直處于繁忙的狀態(tài),導(dǎo)致requestIdlecallback函數(shù)永遠(yuǎn)無(wú)法執(zhí)行回調(diào),瀏覽器提供了一個(gè)額外的setTimeout函數(shù),為這個(gè)任務(wù)設(shè)置截止時(shí)間,瀏覽器就可以根據(jù)這個(gè)截止時(shí)間規(guī)劃這個(gè)任務(wù)的執(zhí)行。

6.參考文章

《Javascript核心原理精講》

《深入淺出Node.js》

《Javascript高級(jí)程序設(shè)計(jì)》

7.寫(xiě)在最后 

本篇文章談了EventLoop在瀏覽器和Node.js中的區(qū)別,EventLoop本身不是什么比較復(fù)雜的概念,只是我們需要根據(jù)JS的不同運(yùn)行平臺(tái),理解它們之間的相同和差異。

 

責(zé)任編輯:武曉燕 來(lái)源: 前端萬(wàn)有引力
相關(guān)推薦

2022-01-04 21:36:33

JS瀏覽器設(shè)計(jì)

2021-05-27 09:00:00

Node.js開(kāi)發(fā)線(xiàn)程

2024-01-05 08:49:15

Node.js異步編程

2012-03-09 09:11:29

Node.js

2021-06-10 07:51:07

Node.js循環(huán)機(jī)制

2021-12-18 07:42:15

Ebpf 監(jiān)控 Node.js

2017-08-16 10:36:10

JavaScriptNode.js事件驅(qū)動(dòng)

2021-09-03 13:42:54

Node.js異步性能

2021-12-09 07:54:19

瀏覽器引擎編譯

2012-02-03 09:25:39

Node.js

2011-09-08 13:46:14

node.js

2020-09-15 08:26:25

瀏覽器緩存

2023-01-31 16:43:31

?Node.js事件循環(huán)

2021-09-26 05:06:04

Node.js模塊機(jī)制

2021-10-15 09:56:10

JavaScript異步編程

2020-12-29 08:21:03

JavaScript微任務(wù)宏任務(wù)

2021-10-22 08:29:14

JavaScript事件循環(huán)

2017-02-09 15:15:54

Chrome瀏覽器

2020-12-23 07:37:17

瀏覽器HTML DOM0

2022-10-17 09:01:09

JavaScripNode.js
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)