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

瀏覽器和 Node.js 的 EventLoop 事件循環機制知多少?

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

[[439223]]

本文轉載自微信公眾號「前端萬有引力」,作者一川 。轉載本文請聯系前端萬有引力公眾號。

1.寫在前面

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

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

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

2.瀏覽器的EventLoop

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

調用棧(Call Stack)

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

事件隊列(Event Queue)

事件隊列:負責將新的函數發送到隊列中進行處理。事件執行隊列符合數據結構中的隊列,先進先出的特性,當先進入的事件先執行,執行完畢先彈出。

 

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

比如,在事件執行隊列操作setTimeout事件時,會現將其發送到瀏覽器對應的API,該API會一直等到約定的時間將其送回調用棧進行處理。即,它將操作發送到事件隊列中,這樣就形成了一個循環系統,用于Javascript中進行異步操作。

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

在EventLoop中,每次循環稱為一次tick。主要順序是:

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

注意:最先進行調用棧的宏任務,一般情況下都是最后返回執行的結果。

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

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

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

宏任務

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

  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事件循環機制中,當執行setTimeout時會將事件進行掛起,執行一些其它的系統任務,當其他的執行完畢之后才會執行,因此執行時間間隔是不可控。

微任務

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

那么微任務是如何產生的呢?

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

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

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

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

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

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

瀏覽器EventLoop的原理是:

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

注意:一次EventLoop循環會處理一個宏任務和所有此處循環中產生的微任務。

3.Node.js的EventLoop

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

Node.js中的事件循環機制

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

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

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

4.Process.nextTick()

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

Process.nextTick會將callback添加到"nextTick queue"隊列中,nextick queue會在當前Javascript stack執行完畢后,下一次EventLoop開始執行前按照FIFO出隊。如果遞歸調用Process.nextTick可能會導致一個無限循環,需要去適當的時機終止遞歸。

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

為什么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.   } 

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

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

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

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

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

5.EventLoop對渲染的影響

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

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

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

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

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

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

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

同時,為了避免瀏覽器一直處于繁忙的狀態,導致requestIdlecallback函數永遠無法執行回調,瀏覽器提供了一個額外的setTimeout函數,為這個任務設置截止時間,瀏覽器就可以根據這個截止時間規劃這個任務的執行。

6.參考文章

《Javascript核心原理精講》

《深入淺出Node.js》

《Javascript高級程序設計》

7.寫在最后 

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

 

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

2022-01-04 21:36:33

JS瀏覽器設計

2021-05-27 09:00:00

Node.js開發線程

2024-01-05 08:49:15

Node.js異步編程

2012-03-09 09:11:29

Node.js

2021-09-03 13:42:54

Node.js異步性能

2021-12-09 07:54:19

瀏覽器引擎編譯

2021-06-10 07:51:07

Node.js循環機制

2021-12-18 07:42:15

Ebpf 監控 Node.js

2017-08-16 10:36:10

JavaScriptNode.js事件驅動

2012-02-03 09:25:39

Node.js

2020-09-15 08:26:25

瀏覽器緩存

2011-09-08 13:46:14

node.js

2023-01-31 16:43:31

?Node.js事件循環

2021-09-26 05:06:04

Node.js模塊機制

2021-10-15 09:56:10

JavaScript異步編程

2020-12-29 08:21:03

JavaScript微任務宏任務

2021-10-22 08:29:14

JavaScript事件循環

2017-02-09 15:15:54

Chrome瀏覽器

2022-10-17 09:01:09

JavaScripNode.js

2023-04-28 15:20:37

JavaScript事件循環
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 四虎永久在线精品免费一区二 | 色婷婷一区二区三区四区 | 一级片在线视频 | 亚洲男人天堂网 | 国内精品视频一区二区三区 | 精品欧美一区二区三区久久久 | 久久久久久影院 | 91香蕉嫩草 | 免费久久久久久 | 欧美色专区 | 国产精品入口麻豆www | 黄网站在线播放 | 99久久婷婷 | 欧美精品乱码久久久久久按摩 | 国产精品久久久久久久久免费高清 | 在线视频一区二区三区 | 久久精品91 | 婷婷久久综合 | 国产精品高潮呻吟久久 | 久久久国产一区 | 狠狠操你 | 亚洲免费网 | 国产xxxx搡xxxxx搡麻豆 | 九九热精品在线 | 国产精品一区在线观看你懂的 | 一级全黄少妇性色生活免费看 | 91av在线视频观看 | 中文字幕三区 | 天天干天天草 | 成人自拍视频 | 精品不卡 | 亚洲欧美在线观看 | 欧美日韩久久精品 | av一级在线观看 | 国产乱码精品一区二区三区五月婷 | 日韩视频中文字幕 | av日韩精品| 亚洲天堂精品久久 | 欧美精品网 | 在线观看中文字幕 | 欧美一区二区三区的 |