首頁  >  文章  >  web前端  >  探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

青灯夜游
青灯夜游轉載
2022-01-05 10:29:572456瀏覽

這篇文章帶大家去探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop,希望對大家有幫助!

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

Event Loop 是JavaScript 的基礎概念,面試必問,平常也常談到,但有沒有想過為什麼會有Event Loop,它為什麼會這樣設計的呢?

今天我們就來探索下原因。

瀏覽器的Event Loop

JavaScript 是用來實作網頁互動邏輯的,涉及dom 操作,如果多個執行緒同時操作需要做同步互斥的處理,為了簡化就設計成了單線程,但是如果單線程的話,遇到定時邏輯、網路請求又會阻塞住。怎麼辦呢?

可以加一層調度邏輯。把 JS 程式碼封裝成一個個的任務,放在一個任務佇列中,主執行緒就不斷的取任務執行就好了。

每次取任務執行,都會建立新的呼叫堆疊。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

其中,計時器、網路請求其實都是在別的執行緒執行的,執行完了之後在任務佇列裡放個任務,告訴主執行緒可以繼續往下執行了。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

因為這些非同步任務是在別的執行緒執行完,然後透過任務佇列通知下主線程,是一種事件機制,所以這個循環叫做 Event Loop。

這些在其他執行緒執行的非同步任務包括計時器(setTimeout、setInterval),UI 渲染、網路請求(XHR 或 fetch)。

但是,現在的 Event Loop 有個嚴重的問題,沒有優先順序的概念,只是按照先後順序來執行,那如果有高優先級的任務就得不到及時的執行了。所以,得設計一套插隊機制。

那就搞一個高優先權的任務佇列就好了,每執行完一個普通任務,都去把所有高優先權的任務給執行完,之後再去執行普通任務。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

有了插隊機制之後,高優任務就能得到及時的執行。

這就是現在瀏覽器的 Event Loop。

其中普通任務叫做 MacroTask(巨集任務),高優任務叫做 MicroTask(微任務)。

巨集任務包含:setTimeout、setInterval、requestAnimationFrame、Ajax、fetch、script 標籤的程式碼。

微任務包括:Promise.then、MutationObserver、Object.observe。

怎麼理解宏微任務的分割呢?

計時器、網路請求這種都是在別的執行緒跑完之後通知主執行緒的普通非同步邏輯,所以都是巨集任務。

而高優任務的這三種也很好理解,MutationObserver 和Object.observe 都是監聽某個物件的變化的,變化是很瞬時的事情,肯定要馬上回應,不然可能又變了,Promise 是組織非同步流程的,非同步結束呼叫then 也是很高優的。

這就是瀏覽器裡的Event Loop 的設計:設計Loop 機制和Task 佇列是為了支援非同步,解決邏輯執行阻塞主執行緒的問題,設計MicroTask 佇列的插隊機制是為了解決高優任務儘早執行的問題。

但後來,JS 的執行環境不只是瀏覽器一種了,還有了 Node.js,它同樣也要解決這些問題,但是它設計出來的 Event Loop 更細緻一些。

Node.js 的Event loop

Node 是一個新的JS 運作環境,它同樣要支援非同步邏輯,包括計時器、 IO、網路請求,很明顯,也可以用Event Loop 那一套來跑。

但是呢,瀏覽器那套 Event Loop 就是為瀏覽器設計的,對於做高效能伺服器來說,那種設計還是有點粗糙了。

哪裡粗糙呢?

瀏覽器的 Event Loop 只分了兩層優先權,一層是巨集任務,一層是微任務。但是宏任務之間沒有再劃分優先級,微任務之間也沒有再劃分優先級。

而Node.js 任務巨集任務之間也是有優先權的,例如定時器Timer 的邏輯就比IO 的邏輯優先權高,因為涉及時間,越早越準確;而close 資源的處理邏輯優先權就很低,因為不close 最多多佔點記憶體等資源,影響不大。

於是就把巨集任務佇列拆成了五個優先權:Timers、Pending、Poll、Check、Close。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

解釋這五種巨集任務:

Timers Callback: 牽涉到時間,肯定越早執行越準確,所以這個優先順序最高很容易理解。

Pending Callback:處理網路、IO 等異常時的回調,有的 *niux 系統會等待發生錯誤的回報,所以得處理下。

Poll Callback:處理 IO 的 data,網路的 connection,伺服器主要處理的就是這個。

Check Callback:執行 setImmediate 的回呼,特點是剛執行完 IO 之後就能回呼這個。

Close Callback:關閉資源的回調,晚點執行影響也不到,優先權最低。

所以呢,Node.js 的Event Loop 就是這樣跑的了:

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

還有一點不同要特別注意:

Node.js 的Event Loop 並不是瀏覽器那種一次執行一個宏任務,然後執行所有的微任務,而是執行完一定數量的Timers 宏任務,再去執行所有微任務,然後再執行一定數量的Pending 的巨集任務,然後再去執行所有微任務,剩餘的Poll、Check、Close 的巨集任務也是這樣。 (訂正:node 11 之前是這樣,node 11 之後改為了每個巨集任務都執行所有微任務了)

為什麼這樣呢?

其實按照優先權來看很容易理解:

假設瀏覽器裡面的巨集任務優先權是1,所以是按照先後順序依序執行,也就是一個巨集任務,所有的微任務,再一個宏任務,再所有的微任務。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

而Node.js 的宏任務之間也是有優先權的,所以Node.js 的Event Loop 每次都是把目前優先權的所有巨集任務跑完再去跑微任務,然後再跑下一個優先順序的巨集任務。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

也就是是一定數量的 Timers 巨集任務,再所有微任務,再一定數量的 Pending Callback 巨集任務,再所有微任務這樣。

為什麼說是一定數量呢?

因為如果某個階段巨集任務太多,下個階段就一直執行不到了,所以有個上限的限制,剩餘的下個 Event Loop 再繼續執行。

除了巨集任務有優先級,微任務也劃分了優先級,多了一個 process.nextTick 的高優先級微任務,在所有的普通微任務之前來跑。

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

所以,Node.js 的Event Loop 的完整流程就是這樣的:

  • Timers 階段:執行一定數量的定時器,也就是setTimeout、setInterval 的callback,太多的話留到下次執行
  • 微任務:執行所有nextTick 的微任務,再執行其他的普通微任務
  • Pending 階段:執行一定數量的IO 和網路的異常回調,太多的話留到下次執行
  • 微任務:執行所有nextTick 的微任務,再執行其他的普通微任務
  • Idle/Prepare 階段:內部用的一個階段
  • 微任務:執行所有nextTick 的微任務,再執行其他的普通微任務
  • Poll 階段:執行一定數量的檔案的data 回呼、網路的connection 回呼,太多的話留到下次執行。 如果沒有IO 回調並且也沒有timers、check 階段的回調要處理,就阻塞在這裡等待IO 事件
  • 微任務:執行所有nextTick 的微任務,再執行其他的普通微任務
  • Check 階段:執行一定數量的setImmediate 的callback,太多的話留到下次執行。
  • 微任務:執行所有nextTick 的微任務,再執行其他的普通微任務
  • Close 階段:執行一定數量的close 事件的callback,太多的話留到下次執行。
  • 微任務:執行所有nextTick 的微任務,再執行其他的普通微任務

比起瀏覽器裡的Event Loop,明顯複雜了很多,但經過我們之前的分析,也能理解:

Node.js 對巨集任務做了優先權劃分,從高到低分別是Timers、Pending、Poll、Check、Close 這5 種,也對微任務做了劃分,也就是nextTick 的微任務和其他微任務。執行流程是先執行完目前優先權的一定數量的巨集任務(剩下的留到下次迴圈),然後執行process.nextTick 的微任務,再執行普通微任務,之後再執行下個優先權的一定數量的宏任務。 。這樣不斷循環。其中還有一個 Idle/Prepare 階段是給 Node.js 內部邏輯用的,不需要關心。

改變了瀏覽器Event Loop 裡那種一次執行一個宏任務的方式,可以讓高優先級的宏任務更早的得到執行,但是也設定了個上限,避免下個階段一直無法執行。

還有一個特別要注意的點,就是poll 階段:如果執行到poll 階段,發現poll 隊列為空並且timers 隊列、check 隊列都沒有任務要執行,那麼就阻塞的等在這裡等IO 事件,而不是空轉。 這點設計也是因為伺服器主要是處理 IO 的,阻塞在這裡可以更早的回應 IO。

完整的Node.js 的Event Loop 是這樣的:

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

#對比下瀏覽器的Event Loop:

探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!

兩個JS 運行環境的Event Loop 整體設計思路是差不多的,只不過Node.js 的Event Loop 對宏任務和微任務做了更細粒度的劃分,也很容易理解,畢竟Node .js 面向的環境和瀏覽器不同,更重要的是服務端對效能的要求會更高。

總結

JavaScript 最早是用來寫網頁互動邏輯的,為了避免多執行緒同時修改dom 的同步問題,設計成了單線程,又為了解決單執行緒的阻塞問題,加了一層調度邏輯,也就是Loop 迴圈和Task 佇列,把阻塞的邏輯放到其他執行緒跑,從而支援了非同步。然後為了支援高優先權的任務調度,又引入了微任務佇列,這就是瀏覽器的 Event Loop 機制:每次執行一個巨集任務,然後執行所有微任務。

Node.js 也是一個JS 運行環境,想支援異步同樣要用Event Loop,只不過服務端環境更複雜,對效能要求更高,所以Node.js 對宏微任務都做了更細粒度的優先權分割:

Node.js 里分割了5 種巨集任務,分別是Timers、Pending、Poll、Check、Close。又劃分了 2 種微任務,分別是 process.nextTick 的微任務和其他的微任務。

Node.js 的Event Loop 流程是執行目前階段的一定數量的巨集任務(剩餘的到下個迴圈執行),然後執行所有微任務,一共有Timers、Pending、Idle/ Prepare、Poll、Check、Close 6 個階段。 (訂正:node 11 之前是這樣,node 11 之後改為了每個巨集任務都執行所有微任務了)

其中 Idle/Prepare 階段是 Node.js 內部用的,不用關心。

特別要注意的是Poll 階段,如果執行到這裡,poll 隊列為空並且timers、check 隊列也為空,就一直阻塞在這裡等待IO,直到timers、check 隊列有回調再繼續loop。

Event Loop 是JS 為了支援非同步和任務優先權而設計的一套排程邏輯,針對瀏覽器、Node.js 等不同環境有不同的設計(主要是任務優先權的劃分粒度不同),Node.js 面對的環境更複雜、對效能要求更高,所以Event Loop 設計的更複雜一些。

更多node相關知識,請造訪:nodejs 教學! !

以上是探索下瀏覽器和 Node.js 為什麼會這樣設計 EventLoop!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除