理解JS中宏任務和微任務


先來一道常見的面試題:

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到底是怎么運行的呢?

 
JS引擎常駐於內存中,等待宿主將JS代碼或函數傳遞給它。也就是等待 宿主環境分配宏觀任務,反復等待 - 執行即為事件循環。  

Event Loop中,每一次循環稱為tick,每一次tick的任務如下:

執行棧選擇最先進入隊列的宏任務(一般都是script),執行其同步代碼直至結束;
檢查是否存在微任務,有則會執行至微任務隊列為空;
如果宿主為瀏覽器,可能會渲染頁面;
開始下一輪tick,執行宏任務中的異步代碼(setTimeout等回調)。

概念5:宏任務和微任務

ES6 規范中,microtask 稱為 jobs,macrotask 稱為 task,宏任務是由宿主發起的,而微任務由JavaScript自身發起。  

在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')


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM