當前位置:
首頁 > 最新 > 一篇文章教會你 Event loop——瀏覽器和 Node

一篇文章教會你 Event loop——瀏覽器和 Node

作者:tomc

https://segmentfault.com/a/1190000013861128

最近對Event loop比較感興趣,所以了解了一下。但是發現整個Event loop儘管有很多篇文章,但是沒有一篇可以看完就對它所有內容都了解的文章。大部分的文章都只闡述了瀏覽器或者Node二者之一,沒有對比的去看的話,認識總是淺一點。所以才有了這篇整理了百家之長的文章。

1. 定義

Event loop:即事件循環,是JavaScript引擎處理非同步任務的方式。說人話就是為了讓單線程的JavaScript通暢的跑起來,所有的非同步操作都要被合適的處理,這個處理邏輯就叫做Event loop。

我們在之前的文章中提到過,JavaScript引擎又稱為JavaScript解釋器,是JavaScript解釋為機器碼的工具,分別運行在瀏覽器和Node中。而根據上下文的不同,Event loop也有不同的實現:其中Node使用了libuv庫來實現Event loop; 而在瀏覽器中,html規範定義了Event loop,具體的實現則交給不同的廠商去完成。

所以,瀏覽器的Event loop和Node的Event loop是兩個概念,下面分別來看一下。

2. 意義

在實際工作中,了解Event loop的意義能幫助你分析一些非同步次序的問題(當然,隨著es7 async和await的流行,這樣的機會越來越少了)。除此以外,它還對你了解瀏覽器和Node的內部機制有積極的作用;對於參加面試,被問到一堆非同步操作的執行順序時,也不至於兩眼抓瞎。

3. 瀏覽器上的實現

在JavaScript中,任務被分為Task(又稱為MacroTask,宏任務)和MicroTask(微任務)兩種。它們分別包含以下內容:

MacroTask: script(整體代碼), setTimeout, setInterval, setImmediate(node獨有), I/O, UI renderingMicroTask: process.nextTick(node獨有), Promises, Object.observe(廢棄), MutationObserver

需要注意的一點是:在同一個上下文中,總的執行順序為同步代碼—>microTask—>macroTask6。這一塊我們在下文中會講。

瀏覽器中,一個事件循環里有很多個來自不同任務源的任務隊列(task queues),每一個任務隊列里的任務是嚴格按照先進先出的順序執行的。但是,因為瀏覽器自己調度的關係,不同任務隊列的任務的執行順序是不確定的。

具體來說,瀏覽器會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否為空(執行完一個task的具體標誌是函數執行棧為空),如果不為空則會一次性執行完所有microtask。然後再進入下一個循環去task隊列中取下一個task執行,以此類推。

注意:圖中橙色的MacroTask任務隊列也應該是在不斷被切換著的。

本段大批量引用了《什麼是瀏覽器的事件循環(Event Loop)》的相關內容,想看更加詳細的描述可以自行取用。

4. Node上的實現

nodejs的event loop分為6個階段,它們會按照順序反覆運行,分別如下:

timers:執行setTimeout() 和 setInterval()中到期的callback。

I/O callbacks:上一輪循環中有少數的I/Ocallback會被延遲到這一輪的這一階段執行

idle, prepare:隊列的移動,僅內部使用

poll:最為重要的階段,執行I/O callback,在適當的條件下會阻塞在這個階段

check:執行setImmediate的callback

close callbacks:執行close事件的callback,例如socket.on("close",func)

不同於瀏覽器的是,在每個階段完成後,而不是MacroTask任務完成後,microTask隊列就會被執行。這就導致了同樣的代碼在不同的上下文環境下會出現不同的結果。我們在下文中會探討。

另外需要注意的是,如果在timers階段執行時創建了setImmediate則會在此輪循環的check階段執行,如果在timers階段創建了setTimeout,由於timers已取出完畢,則會進入下輪循環,check階段創建timers任務同理。

5. 示例5.1 瀏覽器與Node執行順序的區別

在這個例子中,Node的邏輯如下:

最初timer1和timer2就在timers階段中。開始時首先進入timers階段,執行timer1的回調函數,列印timer1,並將promise1.then回調放入microtask隊列,同樣的步驟執行timer2,列印timer2;

至此,timer階段執行結束,event loop進入下一個階段之前,執行microtask隊列的所有任務,依次列印promise1、promise2。

而瀏覽器則因為兩個setTimeout作為兩個MacroTask, 所以先輸出timer1, promise1,再輸出timer2,promise2。

更加詳細的信息可以查閱《深入理解js事件循環機制(Node.js篇)》

為了證明我們的理論,把代碼改成下面的樣子:

按理說 應該比 快,應該只有第二種結果,為什麼會出現兩種結果呢?

這是因為Node 做不到0毫秒,最少也需要1毫秒。實際執行的時候,進入事件循環以後,有可能到了1毫秒,也可能還沒到1毫秒,取決於系統當時的狀況。如果沒到1毫秒,那麼 timers 階段就會跳過,進入 check 階段,先執行setImmediate的回調函數。

另外,如果已經過了Timer階段,那麼setImmediate會比setTimeout更快,例如:

上面代碼會先進入 I/O callbacks 階段,然後是 check 階段,最後才是 timers 階段。因此,setImmediate才會早於setTimeout執行。

具體可以看《Node 定時器詳解》。

5.2 不同非同步任務執行的快慢

因為我們上文說過microTask會優於macroTask運行,所以先輸出下面兩個,而在Node中process.nextTick比Promise更加優先3,所以4在3前。而根據我們之前所說的Node沒有絕對意義上的0ms,所以1,2的順序不固定。

5.3 MicroTask隊列與MacroTask隊列

這個例子來源於《JavaScript中的執行機制》。Promise的代碼是同步代碼,then和catch才是非同步的,所以4要同步輸出,然後Promise的then位於microTask中,優於其他位於macroTask隊列中的任務,所以5會優於1,6輸出,而Timer優於Check階段,所以1,6。

6. 總結

綜上,關於最關鍵的順序,我們要依據以下幾條規則:

同一個上下文下,MicroTask會比MacroTask先運行

然後瀏覽器按照一個MacroTask任務,所有MicroTask的順序運行,Node按照六個階段的順序運行,並在每個階段後面都會運行MicroTask隊列

同個MicroTask隊列下 會優於

Event loop還是比較深奧的,深入進去會有很多有意思的東西,有任何問題還望不吝指出。

參考文檔

《什麼是瀏覽器的事件循環(Event Loop)》

《不要混淆nodejs和瀏覽器中的event loop》

《Node 定時器詳解》

《瀏覽器和Node不同的事件循環(Event Loop)》

《深入理解js事件循環機制(Node.js篇)》

《JavaScript中的執行機制》

覺得本文對你有幫助?請分享給更多人

關注「前端大全」,提升前端技能

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端大全 的精彩文章:

2018年3月十大好玩的CODE PEN

TAG:前端大全 |