寫在前面
寫這篇文章的原因是因爲,這幾天在看 core-js
的源碼,然後發現了 queueMicrotask
的實現。由於之前做的項目,對於微任務的執行需求,一般是使用 asap
這個庫來完成的,如果沒有使用這個庫的話,簡易版本可以通過 Promise.resolve()
來代替,並沒有接觸過過這個 api
,所以就抽時間研究一下。
兼容性
一般看這種偏 web 標準的新的 api
,肯定上來要先看兼容性的,我去 caniuse
查了一下,wtf? 竟然搜索無結果。(詳見 issue)
然後只能去 MDN 來看一下了,大概是下圖這個樣子:
可以發現還是比較新的 api
,如果要在項目中直接使用的話,還是建議導入 polyfill
或者使用 asap
這個庫來實現類似的需求。
爲什麼我們需要這個 api
?
從微任務本身的概念來說的話,就是當我們期望某段代碼,不阻塞當前執行的同步代碼,同時又期望它儘可能快地執行時,我們就需要它(這裏不再贅述微任務的概念,可以參考這篇文章)。
一般情況下,如果是編寫業務代碼,我覺的很少會遇到這樣的需求,唯一能想到的情況可能存在於一些對即時反饋有性能要求的場景,比如搜索,當輸入關鍵字後發送異步請求獲取搜索信息之後,我們可能會在前端對搜索結果進行一些處理,比如排序或者分組,但是這些操作可能不是優先級最高的任務,但它們又比較耗時(比如排序),因此我們可能期望推遲它們的執行,但又期望它們儘可能早地執行。
在閱讀一些著名框架或者工具庫的過程中,我發現很多情況下作者都會遇到這個需求,一般都通過 process.nextTick
或者 Promise.resolve
來解決。
它和 setTimeout
的區別?
本質上的區別應該在它們的執行時機上,而執行時機上的區別,本質上就是微任務和宏任務的區別。可以直接打開控制檯運行一下以下的代碼:
setTimeout(() => {
console.log('setTimeout');
}, 0);
queueMicrotask(() => {
console.log('queueMicrotask');
});
運行結果不出意外應該是:
queueMicrotask
setTimeout
如果你熟悉 nodejs
的話,應該和 process.nextTick
是類似的。
使用其他方式進行模擬所帶來的問題?
這也是我一開始腦海中出現的問題,就是既然我們已經可以通過別的方式來模擬微任務的執行,我們還需要這個 api
幹什麼?比如,通過下面的代碼:
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('queueMicrotask');
});
會得到和上面代碼一樣的運行結果。
這裏引用 Explainer: queueMicrotask 的一些觀點來進行闡述:
- 我們應當使用底層 api 來直接完成類似的功能,而非用頂層 api 進行模擬
- 模擬過程中,對於異常情況,會造成一些困擾,比如
Promise.resolve
會將異常轉化爲一個rejected
的Promise
- 模擬過程中,會創建額外的對象(造成一定意義上的浪費),比如
Promise.resolve
會返回一個Promise
實例對象,而直接queueMicrotask
則不會 - 除了微任務,其他類型的異步任務都有對應的
api
可供使用,比如宏任務、RAF
-
繼上一點的基礎上,語義性會更好,同時幫助開發者理解這些不同異步任務之間的區別
-
setTimeout(callback, 0)
- 宏任務 -
requestAnimationFrame(callback)
- RAF -
queueMicrotask(callback)
- 微任務
-
潛在問題
由於它是一個用於指派微任務的底層 api
,我們很可能會在其中無限制地指派微任務到其隊列之中,這樣做的效果就是,瀏覽器的微任務隊列始終處於非空狀態,這將導致控制權始終無法交還給瀏覽器進行下一次事件循環,然後它就卡死了。
你可以執行下面的代碼來體驗這個現象:
function infiniteEnqueue(fn) {
queueMicrotask(() => infiniteEnqueue(fn))
}
infiniteEnqueue(()=>{})
執行這段代碼會使瀏覽器當前的 tab
卡死,請慎用,建議先打開瀏覽器提供的進程管理窗口以供強制關閉卡死窗口。
關於 polyfill 的不同實現
這裏簡單闡述 MDN
上的和 core-js
中的模擬方案。
MDN
MDN 上的 polyfill 實現比較簡單粗暴,其實和直接調用 Promise.resolve
沒什麼區別,只是會在 .catch
中捕獲錯誤之後再拋出。
core-js
相比較 MDN 的實現,core-js
會複雜一些,它同時考慮了 nodejs
和 browser
兩種情況,同時利用鏈表數據結構來模擬微任務隊列的執行單元,同時實現了一個 flush
方法表示執行全部的微任務單元。
還實現了一個 notify
方法,該方法會根據具體的 js
運行時環境以及 api
的支持情況,分別嘗試使用 process.nextTick
、MutationObserver
和 Promise.resolve
以及最基本的宏任務 api
來執行 flush
方法,變相模擬微任務的執行過程。
參考
關注公衆號 全棧_101,只談技術,不談人生。
另:本人最近比較缺錢,業餘時間接手各種規模的全棧外包項目或者開發任務,有意者私聊。