要想搞明白Javascript的Event Loop我們首先要了解Javascript的運行環境的運行機制。
Javascript是單線程的
我們都知道JS的最大特點是單線程,這就意味著所有的任務需要排隊,后一個任務執行,需要等待前一個任務執行完畢,如果前一個任務執行時間比較久,后一個任務就需要一直等待。等待的過程是可能是處理跟不上或者是硬件I/O效率(比如Ajax操作從網絡讀取數據)導致的。
Javascript語言的設計者為了解決這個等待問題,讓主線程執行的時不管I/O,掛起處于等待中的任務,先運行排在后面的任務。等到I/O返回了結果,再回過頭,把掛起的任務繼續執行下去。這就是異步任務的執行機制。這樣,Javascript就有了同步任務和異步任務兩種。
任務隊列(Macrotask queue和Microtask queue)
Javascript中的同步任務都是在主線程上執行的,異步任務則是由瀏覽器執行的,不管是AJAX請求,還是setTimeout等 API,瀏覽器內核會在其它線程中執行這些操作,當操作完成后,將操作結果以及事先定義的回調函數放入JavaScript 主線程的任務隊列中。
在Javascript的Event Loop機制中,存在兩個任務隊列:Macrotask queue和Microtask queue。
- macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
- microtasks: process.nextTick, Promises, Object.observe(廢棄), MutationObserver
Macrotask還是Microtask?
可以這樣簡單理解:如果你想讓一個異步任務盡快執行,那么就把它設置為Microtask,除此之外都用Macrotask。因為,雖然Javascript是異步非阻塞的,但在一個事件循環中,Microtask的執行方式基本上就是用同步的。
Basically, use microtasks when you need to do stuff asynchronously in a synchronous way (i.e. when you would say perform this (micro-)task in the most immediate future). Otherwise, stick to macrotasks.
可能存在的問題
相信讀到這里你已經意識到,如果一個Microtask隊列太長,或者執行過程中不斷加入新的Microtask任務,會導致下一個Macrotask任務很久都執行不了。結果就是,你可能會遇到UI一直刷新不了,或者I/O任務一直完成不了。
或許是考慮到了這一點,至少Microtask queue中的process.nextTick任務,是被設置了(在一個事件循環中的)最大調用次數process.maxTickDepth的,默認是1000。一定程度上避免了上述情況。
Event Loop
按照 WHATWG 規范,每個Event Loop周期內,會經歷如下步驟:
- 檢查Macrotask queue,把最先入列的Macrotask壓入執行棧(每個Event Loop周期內只會執行一個Macrotask),開始執行,如果Macrotask queue為空,跳到步驟3。
- 待該 Macrotask執行完畢后,清空執行棧。
- 遍歷執行Microtask queue中的Microtask。遍歷這些 Microtask 的過程中,還可以將更多的 Microtask加 入Microtask queue,它們會一一執行,直到整個 Microtask 隊列處理完。
- 執行完Microtask queue中的所有Microtask后,回到步驟1。
以上就是Event Loop的循環機制。
參考
Understanding the Node.js Event Loop
Difference between microtask and macrotask within an event loop context