JS中EventLoop、宏任務與微任務的個人理解
- 2021 年 3 月 31 日
- 筆記
為什麼要EventLoop?
JS 作為瀏覽器腳本語言,為了避免複雜的同步問題(例如用戶操作事件以及操作DOM),這就決定了被設計成單執行緒語言,而且也將會一直保持是單執行緒的。而在單執行緒中若是遇到了耗時的操作(IO,定時器,網路請求)將會一直等待,CPU利用率將會大打折扣,時間大量浪費。所以需要設計一種方案讓一些耗時的操作放在一邊等待,讓後面的函數先執行,於是有了EventLoop的設計。
將任務分為兩種:
- 同步任務
- 非同步任務
- 定時器都是非同步操作
- 事件綁定都是非同步操作
- AJAX中一般採取的非同步操作(雖然也可以同步)
- 回調函數(不嚴謹的非同步)
阮一峰老師《JavaScript 運行機制詳解:再談Event Loop》
(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
(2)主執行緒之外,還存在一個"任務隊列"(task queue)。只要非同步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主執行緒不斷重複上面的第三步。
任務都會按順序進入調用棧(call stack),即圖1-1的stack,然後按棧的順序依次執行。若全是同步任務,就會正常地順序執行。當遇到非同步任務時(其實就是執行到了一個耗時的任務,它發起後,需要它的回調函數等待拿到結果之後才繼續進行)將會放到WebAPIs中(圖1-1),等待這個耗時操作返回結果,也有網友把這個 WebAPIs 稱之為 Event Table。如果非同步任務在WebAPIs中等待有了結果(比如setTimeout的時間截止了,xhr得到響應結果了,用戶click事件發生了),就會將這個結果作為一個事件置於任務隊列中。 【或者稱之為:註冊回調函數】
那麼任務隊列又是什麼?個人認為就是圖中的callback queue,或稱之為 Event Queue 。就是存放了各種耗時操作最後響應結果的各個事件(說白了,就是已經拿到結果的,就會從WebAPIs放到任務隊列里來)
圖 1-1 轉自Philip Roberts的演講《Help, I'm stuck in an event-loop》
搞懂上面兩段話後,就可以談EventLoop的作用了:
- 在調用棧和任務隊列之間進行「輪詢」
- 但輪詢的規則是:只有每當調用棧為空,才能去「詢問」任務隊列中是否有事件需要處理
- 若任務隊列存在事件,則會將該事件相應的回調函數(非同步操作)結束等待,置於調用棧中開始執行
- 如果調用棧一直不為空,那就一直不會「詢問」任務隊列
以上過程是不斷循環的,js引擎中,存在一個叫monitoring process的進程,這個進程會不斷的檢查主執行緒的執行情況,一旦為空,就會去任務隊列檢查有哪些待執行的函數。這裡的整個過程可以參考 一個工具 loupe 對整個調用過程進行查看。

圖 1-2 loupe, 也是從其他地方發現的這個東西,很直觀
針對call stack調用棧多說一句:通俗地講,將調用棧比喻為程式設計師,各個任務比喻為需求,任務隊列比喻為總監。當總監提需求時,程式設計師就要交接需求過來,然後完成它。如果沒有需求,就一直等待總監給需求。給了就做,不給就等。
搞懂同步任務與非同步任務的具體執行流程後,再談談為什麼要設計宏任務和微任務。
為什麼有宏任務、微任務?
頁面渲染事件,各種IO的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,我們不能準確地控制這些事件被添加到任務隊列中的位置。但是這個時候突然有高優先順序的任務需要儘快執行,那麼若只有一種類型的任務就不合適了,所以引入了微任務隊列。
至此,任務隊列已被分為:
- 宏任務隊列,即上文說的任務隊列,callback queue,用於存放宏任務
- 微任務隊列,再開闢一個隊列,用於存放微任務

圖 2-1 微任務Microtask Queue的加入
首先列舉一下哪些是宏任務、哪些是微任務
宏任務:
- script(主程式碼)
- setTimeout()
- setInterval()
- postMessage
- I/O
- UI交互事件
- setImmediate(Node.js)
- requestAnimationFrame(瀏覽器)
微任務:
- new Promise().then(回調)
- MutationObserver(html5 新特性)
- process.nextTick(Node.js)在當前"執行棧"的尾部----下一次Event Loop(主執行緒讀取"任務隊列")之前----觸發回調函數。也就是說,它指定的任務總是發生在所有非同步任務之前
緊接著第一節里說的EventLoop,當時沒有考慮什麼宏任務微任務,現在再加入微任務的概念再來考慮整個流程:
- 依舊是在調用棧和任務隊列中輪詢。(此時的任務隊列指的是宏任務隊列)
- 調用棧為空後,優先檢查微任務隊列,如果微任務隊列中存在事件,則加入到調用棧中進行執行(為什麼先詢問的是微任務隊列而不是宏任務隊列,在後面解釋)
- 註:如果在執行微任務隊列中的函數時,產生了新的微任務(比如then函數嵌套),則會繼續在本次執行中執行(就是說如果期間一直有微任務產生,那就會永遠卡在微任務隊列執行)
- 如果微任務隊列為空,那就取宏任務隊列中的事件加入到調用棧中進行執行
- 若在執行宏任務的時候,產生了新的微任務,就會將該微任務加入到微任務隊列,該微任務隊列將會在下一次宏任務執行之前執行,如圖2-2。
- 循環。
依舊是:兩個任務隊列(宏、微)只有有任務,那麼主進程的調用棧就會調過去執行,沒有任務的話,主進程就一直等著,直到又有任務。

圖 2-2 宏任務與微任務的執行順序
注意的是,圖2-2看起來是宏任務先執行,微任務後執行,這僅僅是宏任務與微任務的先後次序,但不代表宏任務優先順序比微任務高。事實是微任務的優先順序是高於宏任務的。因為微任務其實是產生於宏任務的,不可能憑空產生微任務,也就不可能一開始就出現幾個微任務。在本次宏任務產生微任務後,將會在下次宏任務執行之前,優先執行這些微任務。自然也就映證了設計微任務的初衷:為了讓某些任務儘快執行。
總結完整的EventLoop流程:
- 執行一個宏任務(調用棧中沒有就從宏、微任務隊列中獲取)
- 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
- 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)
- 當前微任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染
- 渲染完畢後,JS執行緒繼續接管,開始下一個宏任務(從事件隊列中獲取)
微任務在本次宏任務之後執行,在本次渲染之前執行,在下次宏任務之前執行。(宏任務 -> 微任務 -> 渲染 -> 宏任務)
包含宏任務、微任務的非同步程式碼分析:
// 知乎作者:Miku // 鏈接://zhuanlan.zhihu.com/p/257069622
// 注意:程式碼中的process.netxTick 函數存在於Node.js中
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5'); }); }); process.nextTick(function() { console.log('6'); }); new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8'); }); setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }); new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12'); }); });
第一輪循環:
1)、首先列印 1 2)、接下來是setTimeout是非同步任務且是宏任務,加入宏任務暫且記為 setTimeout1 3)、接下來是 process 微任務 加入微任務隊列 記為 process1 4)、接下來是 new Promise 裡面直接 resolve(7) 所以列印 7 後面的then是微任務 記為 then1 5)、setTimeout 宏任務 記為 setTimeout2 第一輪循環列印出的是 1 7 當前宏任務隊列:setTimeout1, setTimeout2 當前微任務隊列:process1, then1,第二輪循環:
1)、執行所有微任務 2)、執行process1,列印出 6 3)、執行then1 列印出8 4)、微任務都執行結束了,開始執行第一個宏任務 5)、執行 setTimeout1 也就是 第 3 - 14 行 6)、首先列印出 2 7)、遇到 process 微任務 記為 process2 8)、new Promise中resolve 列印出 4 9)、then 微任務 記為 then2 第二輪循環結束,當前列印出來的是 1 7 6 8 2 4 當前宏任務隊列:setTimeout2 當前微任務隊列:process2, then2第三輪循環:
1)、執行所有的微任務 2)、執行 process2 列印出 3 3)、執行 then2 列印出 5 4)、執行第一個宏任務,也就是執行 setTimeout2 對應程式碼中的 25 - 36 行 5)、首先列印出 9 6)、process 微任務 記為 process3 7)、new Promise執行resolve 列印出 11 8)、then 微任務 記為 then3 第三輪循環結束,當前列印順序為:1 7 6 8 2 4 3 5 9 11 當前宏任務隊列為空 當前微任務隊列:process3,then3第四輪循環:
1)、執行所有的微任務 2)、執行process3 列印出 10 3)、執行then3 列印出 12 程式碼執行結束: 最終列印順序為:1 7 6 8 2 4 3 5 9 11 10 12
參考
- 宏任務與微任務 //zhuanlan.zhihu.com/p/92460508
- js中的宏任務與微任務 //zhuanlan.zhihu.com/p/78113300
- 對微任務和宏任務的執行順序的個人理解 //zhuanlan.zhihu.com/p/257069622
- loupe //latentflip.com/loupe/
- JavaScript 運行機制詳解:再談Event Loop //www.ruanyifeng.com/blog/2014/10/event-loop.html
- 譯文:JS事件循環機制(event loop)之宏任務、微任務 //segmentfault.com/a/1190000014940904
- JavaScript中的Event Loop(事件循環)機制 //segmentfault.com/a/1190000022805523