使用 queueMicrotask 來執行微任務

寫在前面

寫這篇文章的緣由是由於,這幾天在看 core-js 的源碼,而後發現了 queueMicrotask實現。因爲以前作的項目,對於微任務的執行需求,通常是使用 asap 這個庫來完成的,若是沒有使用這個庫的話,簡易版本能夠經過 Promise.resolve() 來代替,並無接觸過這個 api,因此就想着抽時間研究一下。前端

兼容性

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

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

昨天多是拼寫錯誤吧,受網友指點,來補充一下 caniuse 上面的截圖:github

能夠發現仍是比較新的 api,若是要在項目中直接使用的話,仍是建議導入 polyfill 或者使用 asap 這個庫來實現相似的需求。web

爲何咱們須要這個 api

從微任務自己的概念來講的話,就是當咱們指望某段代碼,不阻塞當前執行的同步代碼,同時又指望它儘量快地執行時,咱們就須要它(這裏再也不贅述微任務的概念,能夠參考這篇文章)。api

通常狀況下,若是是編寫業務代碼,我覺的不多會遇到這樣的需求,惟一能想到的狀況可能存在於一些對即時反饋有性能要求的場景,好比搜索,當輸入關鍵字後發送異步請求獲取搜索信息以後,咱們可能會在前端對搜索結果進行一些處理,好比排序或者分組,可是這些操做可能不是優先級最高的任務,但它們又比較耗時(好比排序),所以咱們可能指望推遲它們的執行,但又指望它們儘量早地執行。瀏覽器

在閱讀一些著名框架或者工具庫的過程當中,我發現不少狀況下做者都會遇到這個需求,通常都經過 process.nextTick 或者 Promise.resolve 來解決。bash

它和 setTimeout 的區別?

本質上的區別應該在它們的執行時機上,而執行時機上的區別,本質上就是微任務和宏任務的區別。能夠直接打開控制檯運行一下如下的代碼:markdown

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,只談技術,不談人生。


業餘時間接手各類規模的外包項目,有意者私信。

相關文章
相關標籤/搜索