使用 queueMicrotask 來執行微任務

寫在前面

寫這篇文章的原因是因爲,這幾天在看 core-js 的源碼,然後發現了 queueMicrotask實現。由於之前做的項目,對於微任務的執行需求,一般是使用 asap 這個庫來完成的,如果沒有使用這個庫的話,簡易版本可以通過 Promise.resolve() 來代替,並沒有接觸過過這個 api,所以就抽時間研究一下。

兼容性

一般看這種偏 web 標準的新的 api,肯定上來要先看兼容性的,我去 caniuse 查了一下,wtf? 竟然搜索無結果。(詳見 issue

然後只能去 MDN 來看一下了,大概是下圖這個樣子:

clipboard.png

可以發現還是比較新的 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 會將異常轉化爲一個 rejectedPromise
  • 模擬過程中,會創建額外的對象(造成一定意義上的浪費),比如 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 會複雜一些,它同時考慮了 nodejsbrowser 兩種情況,同時利用鏈表數據結構來模擬微任務隊列的執行單元,同時實現了一個 flush 方法表示執行全部的微任務單元。

還實現了一個 notify 方法,該方法會根據具體的 js 運行時環境以及 api 的支持情況,分別嘗試使用 process.nextTickMutationObserverPromise.resolve 以及最基本的宏任務 api 來執行 flush 方法,變相模擬微任務的執行過程。

參考


關注公衆號 全棧_101,只談技術,不談人生。

clipboard.png


另:本人最近比較缺錢,業餘時間接手各種規模的全棧外包項目或者開發任務,有意者私聊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章