摘要:如果沒到毫秒,那么階段就會跳過,進入階段,先執行的回調函數。參考文檔什么是瀏覽器的事件循環不要混淆和瀏覽器中的定時器詳解瀏覽器和不同的事件循環深入理解事件循環機制篇中的執行機制
最近對Event loop比較感興趣,所以了解了一下。但是發現整個Event loop盡管有很多篇文章,但是沒有一篇可以看完就對它所有內容都了解的文章。大部分的文章都只闡述了瀏覽器或者Node二者之一,沒有對比的去看的話,認識總是淺一點。所以才有了這篇整理了百家之長的文章。1. 定義
Event loop:為了協調事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網絡(networking)等,用戶代理(user agent)必須使用事件循環(event loops)。(3月29修訂)
那什么是事件?
事件:事件就是由于某種外在或內在的信息狀態發生的變化,從而導致出現了對應的反應。比如說用戶點擊了一個按鈕,就是一個事件;HTML頁面完成加載,也是一個事件。一個事件中會包含多個任務。
我們在之前的文章中提到過,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 rendering
MicroTask: process.nextTick(node獨有), Promises, Object.observe(廢棄), MutationObserver
需要注意的一點是:在同一個上下文中,總的執行順序為同步代碼—>microTask—>macroTask[6]。這一塊我們在下文中會講。
瀏覽器中,一個事件循環里有很多個來自不同任務源的任務隊列(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執行順序的區別setTimeout(()=>{ console.log("timer1") Promise.resolve().then(function() { console.log("promise1") }) }, 0) setTimeout(()=>{ console.log("timer2") Promise.resolve().then(function() { console.log("promise2") }) }, 0) 瀏覽器輸出: time1 promise1 time2 promise2 Node輸出: time1 time2 promise1 promise2
在這個例子中,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篇)》
為了證明我們的理論,把代碼改成下面的樣子:
setImmediate(() => { console.log("timer1") Promise.resolve().then(function () { console.log("promise1") }) }) setTimeout(() => { console.log("timer2") Promise.resolve().then(function () { console.log("promise2") }) }, 0) Node輸出: timer1 timer2 promise1 或者 promise2 timer2 timer1 promise2 promise1
按理說setTimeout(fn,0)應該比setImmediate(fn)快,應該只有第二種結果,為什么會出現兩種結果呢?
這是因為Node 做不到0毫秒,最少也需要1毫秒。實際執行的時候,進入事件循環以后,有可能到了1毫秒,也可能還沒到1毫秒,取決于系統當時的狀況。如果沒到1毫秒,那么 timers 階段就會跳過,進入 check 階段,先執行setImmediate的回調函數。
另外,如果已經過了Timer階段,那么setImmediate會比setTimeout更快,例如:
const fs = require("fs"); fs.readFile("test.js", () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });
上面代碼會先進入 I/O callbacks 階段,然后是 check 階段,最后才是 timers 階段。因此,setImmediate才會早于setTimeout執行。
具體可以看《Node 定時器詳解》。
5.2 不同異步任務執行的快慢setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); Promise.resolve().then(() => console.log(3)); process.nextTick(() => console.log(4)); 輸出結果:4 3 1 2或者4 3 2 1
因為我們上文說過microTask會優于macroTask運行,所以先輸出下面兩個,而在Node中process.nextTick比Promise更加優先[3],所以4在3前。而根據我們之前所說的Node沒有絕對意義上的0ms,所以1,2的順序不固定。
5.3 MicroTask隊列與MacroTask隊列setTimeout(function () { console.log(1); },0); console.log(2); process.nextTick(() => { console.log(3); }); new Promise(function (resolve, rejected) { console.log(4); resolve() }).then(res=>{ console.log(5); }) setImmediate(function () { console.log(6) }) console.log("end"); Node輸出: 2 4 end 3 5 1 6
這個例子來源于《JavaScript中的執行機制》。Promise的代碼是同步代碼,then和catch才是異步的,所以4要同步輸出,然后Promise的then位于microTask中,優于其他位于macroTask隊列中的任務,所以5會優于1,6輸出,而Timer優于Check階段,所以1,6。
6. 總結綜上,關于最關鍵的順序,我們要依據以下幾條規則:
同一個上下文下,MicroTask會比MacroTask先運行
然后瀏覽器按照一個MacroTask任務,所有MicroTask的順序運行,Node按照六個階段的順序運行,并在每個階段后面都會運行MicroTask隊列
同個MicroTask隊列下process.tick()會優于Promise
Event loop還是比較深奧的,深入進去會有很多有意思的東西,有任何問題還望不吝指出。
參考文檔:《什么是瀏覽器的事件循環(Event Loop)》
《不要混淆nodejs和瀏覽器中的event loop》
《Node 定時器詳解》
《瀏覽器和Node不同的事件循環(Event Loop)》
《深入理解js事件循環機制(Node.js篇)》
《JavaScript中的執行機制》
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93543.html
摘要:前段時間我對于瀏覽器中的和哪個先執行有所困惑,苦于搜索也沒有發現很明確的答案,于是決定深入探索瀏覽器,現有所愚見,想與大家分享,希望能幫助到那些還在爬坑的人。瀏覽器端中的異步隊列有兩種隊列和隊列。瀏覽器會不斷從隊列中按順序取執行。 前段時間我對于瀏覽器Event loop中的MacroTask和MicroTask哪個先執行有所困惑,苦于搜索也沒有發現很明確的答案,于是決定深入探索瀏覽器...
摘要:由于兩個都是異步函數,按照執行順序,先將放到,接著將移到,因為在指定要秒后才執行,所以先于到注冊回調函數到,所以輸出的結果是。 眾所周知,Javascript是單線程語言, 這就意味著,所有的任務都必須按照順序執行,只有等前面的一個任務執行完畢了,下一個任務才能執行。如果前面一個任務耗時很長,后一個任務就得一直等著,因此,為了實現主線程的不阻塞,就有了Event Loop。 1、jav...
前言 我在學習瀏覽器和NodeJS的Event Loop時看了大量的文章,那些文章都寫的很好,但是往往是每篇文章有那么幾個關鍵的點,很多篇文章湊在一起綜合來看,才可以對這些概念有較為深入的理解。 于是,我在看了大量文章之后,想要寫這么一篇博客,不采用官方的描述,結合自己的理解以及示例代碼,用最通俗的語言表達出來。希望大家可以通過這篇文章,了解到Event Loop到底是一種什么機制,瀏覽器和Nod...
摘要:令人困惑的是,文檔中稱,指定的回調函數,總是排在前面。另外,由于指定的回調函數是在本次事件循環觸發,而指定的是在下次事件循環觸發,所以很顯然,前者總是比后者發生得早,而且執行效率也高因為不用檢查任務隊列。 一、定時器 除了放置異步任務的事件,任務隊列還可以放置定時事件,即指定某些代碼在多少時間之后執行。這叫做定時器(timer)功能,也就是定時執行的代碼。 定時器功能主要由setTim...
摘要:前言以異步和事件驅動的特性著稱但異步是怎么實現的呢其中核心的一部分就是下文中內容基本來自于文檔有不準確地方請指出什么是能讓的操作表現得無阻塞盡管是單線程的但通過盡可能的將操作放到操作系統內核由于現在大多數內核都是多線程的它們可以在后臺執行多 前言 Node.js以異步I/O和事件驅動的特性著稱,但異步I/O是怎么實現的呢?其中核心的一部分就是event loop,下文中內容基本來自于N...
閱讀 1536·2023-04-26 02:08
閱讀 3128·2021-10-14 09:42
閱讀 7177·2021-09-22 15:34
閱讀 3236·2019-08-30 13:16
閱讀 2719·2019-08-26 13:49
閱讀 1342·2019-08-26 11:59
閱讀 1252·2019-08-26 10:31
閱讀 2170·2019-08-23 17:19