先來一道常見的面試題:
console.log('start') setTimeout(() => { console.log('setTimeout') }, 0) new Promise((resolve) => { console.log('promise') resolve() }) .then(() => { console.log('then1') }) .then(() => { console.log('then2') }) console.log('end')
應該不少同學都能答出來,結果為:
start promise end then1 then2 setTimeout
這個就涉及到JavaScript事件輪詢中的宏任務和微任務。那么,你能說清楚到底宏任務和微任務是什么?是誰發起的?為什么微任務的執行要先於宏任務呢?
首先,我們需要先知道js運行機制。
js運行機制
概念1: JS是單線程執行
”JS是單線程的”指的是JS 引擎線程。
在瀏覽器環境中,有JS 引擎線程和渲染線程,且兩個線程互斥。
Node環境中,只有JS 線程。
概念2:宿主
JS運行的環境。一般為瀏覽器或者Node。
概念3:執行棧
是一個存儲函數調用的棧結構,遵循先進后出的原則。
function foo() { throw new Error('error') } function bar() { foo() } bar()

當開始執行 JS 代碼時,首先會執行一個 main 函數,然后執行我們的代碼。根據先進后出的原則,后執行的函數會先彈出棧,在圖中我們也可以發現,foo 函數后執行,當執行完畢后就從棧中彈出了。
概念4:Event Loop
JS到底是怎么運行的呢?

Event Loop中,每一次循環稱為tick,每一次tick的任務如下:
執行棧選擇最先進入隊列的宏任務(一般都是script),執行其同步代碼直至結束;
檢查是否存在微任務,有則會執行至微任務隊列為空;
如果宿主為瀏覽器,可能會渲染頁面;
開始下一輪tick,執行宏任務中的異步代碼(setTimeout等回調)。
概念5:宏任務和微任務
在ES3以及以前的版本中,JavaScript本身沒有發起異步請求的能力,也就沒有微任務的存在。在ES5之后,JavaScript引入了Promise,這樣,不需要瀏覽器,JavaScript引擎自身也能夠發起異步任務了。
所以,總結一下,兩者區別為:
宏任務(macrotask) | 微任務(microtask) | |
---|---|---|
誰發起的 | 宿主(Node、瀏覽器) | JS引擎 |
具體事件 | 1. script (可以理解為外層同步代碼) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js) |
1. Promise 2. MutaionObserver 3. Object.observe(已廢棄;Proxy 對象替代) 4. process.nextTick(Node.js) |
誰先運行 | 后運行 | 先運行 |
會觸發新一輪Tick嗎 | 會 | 不會 |
拓展 1:async和await是如何處理異步任務的?
簡單說,async是通過Promise包裝異步任務。
比如有如下代碼:
async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1()
改為ES5的寫法:
new Promise((resolve, reject) => { // console.log('async2 end') async2() ... }).then(() => { // 執行async1()函數await之后的語句 console.log('async1 end') })
當調用 async1 函數時,會馬上輸出 async2 end,並且函數返回一個 Promise,接下來在遇到 await的時候會就讓出線程開始執行 async1 外的代碼(可以把 await 看成是讓出線程的標志)。
然后當同步代碼全部執行完畢以后,就會去執行所有的異步代碼,那么又會回到 await 的位置,去執行 then 中的回調。
拓展 2:setTimeout,setImmediate誰先執行?
setImmediate和process.nextTick為Node環境下常用的方法(IE11支持setImmediate),所以,后續的分析都基於Node宿主。
Node.js是運行在服務端的js,雖然用到也是V8引擎,但由於服務目的和環境不同,導致了它的API與原生JS有些區別,其Event Loop還要處理一些I/O,比如新的網絡連接等,所以與瀏覽器Event Loop不太一樣。
執行順序如下:
timers: 執行setTimeout和setInterval的回調
pending callbacks: 執行延遲到下一個循環迭代的 I/O 回調
idle, prepare: 僅系統內部使用
poll: 檢索新的 I/O 事件;執行與 I/O 相關的回調。事實上除了其他幾個階段處理的事情,其他幾乎所有的異步都在這個階段處理。
check: setImmediate在這里執行
close callbacks: 一些關閉的回調函數,如:socket.on('close', ...)
一般來說,setImmediate會在setTimeout之前執行,如下:
console.log('outer'); setTimeout(() => { setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); }, 0);
其執行順序為:
外層是一個setTimeout,所以執行它的回調的時候已經在timers階段了
處理里面的setTimeout,因為本次循環的timers正在執行,所以其回調其實加到了下個timers階段
處理里面的setImmediate,將它的回調加入check階段的隊列
外層timers階段執行完,進入pending callbacks,idle, prepare,poll,這幾個隊列都是空的,所以繼續往下
到了check階段,發現了setImmediate的回調,拿出來執行
然后是close callbacks,隊列是空的,跳過
又是timers階段,執行console.log('setTimeout')
但是,如果當前執行環境不是timers階段,就不一定了。。。。順便科普一下Node里面對setTimeout的特殊處理:setTimeout(fn, 0)會被強制改為setTimeout(fn, 1)。
看看下面的例子:
setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); });
其執行順序為:
遇到setTimeout,雖然設置的是0毫秒觸發,但是被node.js強制改為1毫秒,塞入times階段
遇到setImmediate塞入check階段
同步代碼執行完畢,進入Event Loop
先進入times階段,檢查當前時間過去了1毫秒沒有,如果過了1毫秒,滿足setTimeout條件,執行回調,如果沒過1毫秒,跳過
跳過空的階段,進入check階段,執行setImmediate回調
可見,1毫秒是個關鍵點,所以在上面的例子中,setImmediate不一定在setTimeout之前執行了。
拓展 3:Promise,process.nextTick誰先執行?
因為process.nextTick為Node環境下的方法,所以后續的分析依舊基於Node。
process.nextTick() 是一個特殊的異步API,其不屬於任何的Event Loop階段。事實上Node在遇到這個API時,Event Loop根本就不會繼續進行,會馬上停下來執行process.nextTick(),這個執行完后才會繼續Event Loop。
所以,nextTick和Promise同時出現時,肯定是nextTick先執行,原因是nextTick的隊列比Promise隊列優先級更高。
拓展 4:應用場景 - vue中的vm.$nextTick
vm.$nextTick 接受一個回調函數作為參數,用於將回調延遲到下次DOM更新周期之后執行。
這個API就是基於事件循環實現的。
“下次DOM更新周期”的意思就是下次微任務執行時更新DOM,而vm.$nextTick就是將回調函數添加到微任務中(在特殊情況下會降級為宏任務)。
因為微任務優先級太高,vue 2.4版本之后,提供了強制使用宏任務的方法。
vm.$nextTick優先使用Promise,創建微任務。
如果不支持Promise或者強制開啟宏任務,那么,會按照如下順序發起宏任務:http://www.ssnd.com.cn 化妝品OEM代加工
優先檢測是否支持原生 setImmediate(這是一個高版本 IE 和 Edge 才支持的特性)
如果不支持,再去檢測是否支持原生的MessageChannel
如果也不支持的話就會降級為 setTimeout。
小結
下面是道加強版的考題,大家可以試一試。
console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end')