EventLoop是什麼
EventLoop是一個代碼執行模型,它規定了代碼的執行順序,nodeJs和JavaScript擁有各自的EventLoop,了解EventLoop能讓我們更加了解代碼的執行,更好地掌控代碼
宏任務和微任務
JavaScript中的代碼執行分為同步執行和異步執行,而異步執行的代碼中又分為宏任務和微任務,它們的執行順序有所不同。它們并沒有什麼概念性的差别,隻是根據執行順序的不同而進行區分,僅此而已。是以,隻需要硬性的記住哪些是宏任務哪些是微任務就可以了。
宏任務(macrotask/task):
執行順序為同步和微任務之後,宏任務具體的種類有:
- setTimeout
- setInterval
- setImmediate (Node)
- requestAnimationFrame (浏覽器)
- I/O
- UI rendering (浏覽器獨有)
微任務(microtask/jobs):
執行順序為同步之後,宏任務之前,具體的種類有:
- process.nextTick (Node)
- Promise的then和catch中的代碼
- Object.observe(已廢棄)
- MutationObserver
javaScript EventLoop
簡單了解的講解
在JavaScript中,代碼分為同步代碼和異步代碼,異步代碼又分為宏任務和微任務。在eventLoop模型中,規定了這些代碼的執行順序:
- 首先按照順序執行所有的同步代碼
- 執行同步代碼碰到了微任務,将其按順序從後按照隊列的形式放入目前執行棧中待執行(參考執行棧1的結構)
- 執行同步代碼碰到了宏任務,将其按順序從前按照隊列的形式放入下一個執行棧中待執行
- 所有同步代碼執行完畢,開始按順序執行微任務
- 所有微任務執行完畢,開啟下一個執行棧按順序執行宏任務
- 宏任務執行中碰到了微任務,将微任務按順序從後按照隊列的形式放入目前執行棧中待執行(參考執行棧2的結構)
- 執行完目前宏任務後執行所有新的微任務
- 新的微任務執行完畢,開啟下一個執行棧繼續執行宏任務(參考執行棧3的結構)
- 反複執行6-8的步驟直到宏任務全部完成
由上面的步驟可見,在執行完同步代碼後,代碼的執行步驟形成了一個循環圈,這個循環圈就是所謂的EventLoop
實際上真實運作的标準講解
真實的eventLoop模型的運作,執行結構分為3部分:主執行棧Stack,宏任務隊列Task Queue,微任務隊列Microtask Queue。代碼的執行順序為:
- 所有的同步代碼按順序放入主執行棧中執行
- 執行同步代碼的時候碰到了微任務,将微任務放入微任務隊列中
- 執行同步代碼的時候碰到了宏任務,将宏任務放入宏任務隊列中
- 所有同步代碼執行完畢,從微任務隊列中按順序取出微任務放入主執行棧中依次執行,直到微任務隊列為空
- 所有微任務執行完畢,從宏任務隊列中取出隊首的宏任務放入主執行棧中執行
- 如果在執行宏任務的時候碰到了微任務則将其加入微任務隊列中
- 目前宏任務執行完畢,檢視微任務隊列中是否有新增的微任務,如果有則按順序取出放入主執行棧中執行,如果沒有則從宏任務隊列中取出下一個宏任務放入主執行棧中執行
- 反複執行 5-8直到完成所有宏任務
由上面的步驟可見,在執行完同步代碼後,代碼的執行步驟形成了一個循環圈,這個循環圈就是所謂的EventLoop
NodeJs EventLoop
先附上官方文檔連接配接
https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
我自己通讀完全原文後差不多是可以全了解了eventloop的運作和其中一些api的差别了,講的非常的詳細
然後附上講解
NodeJs中的 EventLoop 與 浏覽器JavaScript的EventLoop相比,大體上相同,細看完全不同,宏觀上來說,都是先執行完同步代碼後執行異步代碼,先執行微任務後執行宏任務的這麼一個循環
但是放大來看,在具體的運作之中它們在循環方面其實并不相同:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
這裡是官方給出的标準NodeJs EventLoop運作模型。這裡先對EventLoop的不同層級進行講解:
- timer:存放所有
和setTimeout
中定義的回調函數的隊列,需要注意的是,這裡執行的計時器和間隔器的回調并不是嚴格按照它設定的時間來的,一般來說它會受到poll階段運作的影響setInterval
- pending callbacks:存放執行一些系統操作的回調的隊列,一些錯誤報告之類的都會在這裡進行執行報告
- idle, prepare:僅供node内部使用
- poll:I/O操作回調函數存放隊列
- check:
函數定義的回調存放隊列setImmediate()
- close callbacks:存放與socket的關閉(close)和摧毀(destroy)有關的回調的隊列
以上的每一個隊列都代表EventLoop的一個階段,而每個隊列裡的回調任務都代表宏任務,且每個隊列都具有執行上的最大長度(node為了防止eventLoop一直卡在某一個階段而設定的執行上限,防止把别的階段特别是I/O餓死)。
NodeJs我看他官方文檔上來說是沒有提到微任務這個點的,但是它講了一些别的東西,那些東西的執行特征和微任務的執行特征基本一緻,而且還有一個Promise的微任務(雖然我在Node中從沒有單獨用過,但是axios是可以在服務端送出請求的,而axios是基于promise的),是以我就着我讀過文檔後的了解(可能會出錯,畢竟隻全英原文讀了3遍,加上個人經驗可能并不是很足,如果出錯了還請告訴我)來給出一個微任務的隊列結構
同浏覽器的JavaScript一般,Node的微任務同樣是在每一次宏任務執行完畢之後都會進行一次執行,放在上圖的結構中就是在每一個EventLoop的階段執行完畢後,下一個階段開始之前都會對微任務的隊列進行一次清空性的執行,其中優先執行process.nextTick所放入的回調函數。
将各方面簡單介紹過後,以下将對NodeJs的EventLoop循環進行解釋:
- 執行所有的同步代碼
- 進入timers階段,檢視timers隊列中是否有注冊的回調,如果有則清空性執行
- 清空性執行微任務隊列
- 進入pending階段,檢視是否有錯誤報告回調注冊,如果有則清空性執行
- 清空性執行微任務隊列
- 進入prepare階段,僅供node内部使用,不開展讨論
- 清空性執行微任務隊列
- 進入poll階段,執行所有注冊了的I/O回調,具體的在下文單獨叙述
- 清空性執行微任務隊列
- 進入check階段,清空性執行
注冊的回調setImmediate
- 清空性執行微任務隊列
- 進入close階段,檢視是否有socket的關閉(close)或摧毀(destroy)事件觸發,有則執行
- 清空性執行微任務隊列
- 反複執行2-13
以上的執行循環就是整個NodeJs的EventLoop
EventLoop poll 階段
在node中的EventLoop,這個階段的執行是相對複雜的,且這個階段的執行一定程度上影響着别的階段的執行(因為node有時候會自動阻塞在這裡)。這個階段主要是執行I/O操作的回調,當EventLoop來到這個階段且timer隊列是空的情況,會發生如下幾種情況:
- 如果poll隊列是非空的情況,會正常清空性執行所有隊列中的回調函數直到隊列為空或者達到執行上限,之後EventLoop向下循環
- 如果poll隊列是空的情況,又會發生兩種情況:
- 如果check隊列是非空狀态,則node會立即終止poll階段,EventLoop正常向下循環運轉
- 如果check隊列是空的,且check和timer隊列沒有回調任務,node将在此處阻塞,等待I/O操作完成,并立即執行所有新添加的I/O回調
node在此階段的阻塞并不影響EventLoop的運作,因為每次poll隊列為空的時候node都會檢查timer隊列是否有新的回調任務,如果有新的回調任務EventLoop會正常向下循環,保證了timer回調的執行。
timer階段一定程度上受poll階段影響
在timer的注冊中,回調任務的執行會有一定的間隔,假設設定了一個setTimeout,100ms後執行回調,node并不會傻傻的在這裡等,此時它會檢查poll隊列是否有能在100ms完成的事(即EventLoop向下循環),假設有個poll中有個I/O執行出結果用了95ms,然後回調執行會用10ms,此時95ms小于100ms,是以node會果斷的去執行poll中的I/O回調,執行完後才執行timer注冊的回調(即EventLoop再向下循環回到timer),但是此時時間已經過去了105ms,是以timer的回調執行并不是嚴格按照設定的時間間隔執行,會有一定的偏差。
如果你已經了解了上面的poll階段解釋,這裡就不難了解。
Node中的setTimeout、setInterval和setImmediate
setTimeout/setInterval和setImmediate在node中的使用是有差別的:
- setTimeout與setInterval的回調函數會放在timer隊列中執行
- setImmediate的回調會放在check隊列中執行
雖然話是這麼說,但是它們的運作順序在不同情況下是不同的,分為2種情況:
兩者都在主子產品且其中沒有涉及到I/O操作的時候,它們的運作先後順序是不确定的,誰先運作誰後運作很大程度上依賴于目前EventLoop所處的階段。以下舉個例子:
- 一種情況:你在Poll階段分别注冊了setTimeout和setImmediate,那麼根據EventLoop的運作順序就是先check階段後timer階段,即先setImmediate後setTimeout/setInterval
- 另一種情況:你在close階段注冊了setTimeout和setImmediate,那麼根據EventLoop的運作順序就是先timer階段後check階段,即先setTimeout/setInterval後setImmediate
但是,一旦其中涉及到了I/O操作,不管你用setTimeout/setInterval向timer中注冊了多少回調,setImmediate總是會比setTimeout/setInterval先執行
setImmediate和process.nextTick
在node中,process.nextTick允許你主動地添加一個微異步回調,每個EventLoop階段末尾會清空性執行微任務隊列。
一般來說,使用它的情景有:
- 需要手動報錯(雖然這個錯誤不影響下面代碼執行,但是必須要報告)的時候
- 需要清理一些不需要的資源的時候
- 需要在下一個階段之前嘗試重新發送請求的時候
- 有函數需要使用微異步解決的時候(也就是同步解決不了問題,但是它一定要在下個階段前執行的時候)
除了以上場景之外都不推薦使用它,因為使用它是有風險的,需要謹慎使用,一旦你遞歸使用process.nextTick,就會導緻EventLoop遲遲無法運作到下一個階段,這會導緻你的I/O操作餓死,而且相對于setImmediate來說,它的執行不是那麼好掌控。是以大部分情況都推薦使用setImmediate而非process.nextTick。