成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

[[439223]]

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

1.寫在前面

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

那么,我們得思考幾個問題:

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

2.瀏覽器的EventLoop

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

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

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

事件隊列(Event Queue)

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

 

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

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

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

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

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

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

事實上,EventLoop通過內(nèi)部兩個隊列來實現(xiàn)Event Queue放進來的異步任務。以setTimeout為代表的任務稱為宏任務,放在宏任務隊列(Macrotask Queue)中;以Promise為代表的任務稱為微任務,放在微任務隊列(Microtask Queue)中。

主要的宏任務和微任務有:

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

宏任務

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

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

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

微任務

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

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

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

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

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

MutationObserver采用了"異步+微任務"策略,通過異步操作解決了同步操作的性能問題,通過微任務解決了實時性問題。

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

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

瀏覽器EventLoop的原理是:

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

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

3.Node.js的EventLoop

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

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

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

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

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

4.Process.nextTick()

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

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

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

為什么Process.nextTick這樣的API會被允許存在于Nodejs中呢?部分原因是因為設計理念,在nodejs中api總是異步的,即使那些不需要異步的地方。

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

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

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

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

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

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

5.EventLoop對渲染的影響

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

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

簡而言之,就是在每次EventLoop結(jié)束前,判斷當前是否有渲染時機即重新渲染,而渲染時機是有屏幕限制的,瀏覽器的刷新幀率是60Hz,即1s內(nèi)刷新了60次。此時瀏覽器的渲染時間就沒必要小于16.6ms,因為渲染了屏幕也不會進行展示,

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

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

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

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

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

6.參考文章

《Javascript核心原理精講》

《深入淺出Node.js》

《Javascript高級程序設計》

7.寫在最后 

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

 

責任編輯:武曉燕 來源: 前端萬有引力
相關推薦

2022-01-04 21:36:33

JS瀏覽器設計

2021-05-27 09:00:00

Node.js開發(fā)線程

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)機制

2021-12-09 07:54:19

瀏覽器引擎編譯

2021-09-03 13:42:54

Node.js異步性能

2017-08-16 10:36:10

JavaScriptNode.js事件驅(qū)動

2021-12-18 07:42:15

Ebpf 監(jiān)控 Node.js

2011-09-08 13:46:14

node.js

2012-02-03 09:25:39

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模塊機制

2020-12-29 08:21:03

JavaScript微任務宏任務

2021-10-15 09:56:10

JavaScript異步編程

2021-10-22 08:29:14

JavaScript事件循環(huán)

2017-02-09 15:15:54

Chrome瀏覽器

2022-10-17 09:01:09

JavaScripNode.js

2023-04-28 15:20:37

JavaScript事件循環(huán)
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品亚洲欧美日韩一区在线 | 五月天国产视频 | 久久精品伊人 | 伊人久操| 国产一二区视频 | 国内精品久久影院 | 日本高清不卡视频 | 欧美性影院 | 超碰男人天堂 | 久久蜜桃av一区二区天堂 | 精品国产乱码久久久久久1区2区 | 久久99精品久久久久久琪琪 | 超碰免费在线 | 久久国产精品99久久久大便 | 亚洲国产欧美一区 | 一本一道久久a久久精品综合蜜臀 | 99久久久国产精品免费消防器 | 国产午夜精品一区二区三区四区 | avhd101在线成人播放 | 久久亚洲欧美日韩精品专区 | 99久久婷婷国产亚洲终合精品 | 亚洲午夜精品一区二区三区 | 久操福利| 亚洲444kkkk在线观看最新 | 国产精品久久久久久久久久免费看 | 欧美日韩综合精品 | 青娱乐av | 国产午夜精品视频 | 欧美三级免费观看 | 伊人精品 | 91精品国产综合久久久久 | 欧美成人综合 | 免费成人毛片 | 国产一在线 | 久久综合九九 | 国产亚洲www | 久久久久网站 | 亚洲精品二区 | 国产视频三区 | 亚洲欧美一区二区三区视频 | 日本三级做a全过程在线观看 |