熱門:瘦小腿瘦小腿瘦小腿

  1. 首頁
  2. 科技日報
  3. 網路

我與 Microtasks 的前世今生之一眼望穿千年

  • 小白兔

  • 2018-10-23 09:26:54

本文由 IMWeb 團隊成員 shijisun首發於 IMWeb 社群網站 imweb.io。點選閱讀原文檢視 IMWeb 社群更多精彩文章。

本文有標題黨之嫌,內含大量Microtaks相關總結性資訊,請謹慎服用。

Google Developer Day China 2018 by Jake Archibald

2018年9月21日,雖然沒有參加該場GDD,但是也有幸拜讀了百度@小蘑菇小哥總結的文章深入瀏覽器的事件迴圈(GDD@2018),配注的說明插圖形象生動,文終的click程式碼也很有意思,推薦大家閱讀。這裡就先恬不知恥的將該文的精華以及一些自己的總結陳列如下:

- 當次事件迴圈產生的新任務會在指定時機加入任務佇列等待執行

- setInterval

- setImmediate

- I/O

- 當次事件迴圈執行佇列裡的所有任務

- 當次事件迴圈產生的新任務會在下一次迴圈執行

- 當次事件迴圈執行佇列裡的所有任務

- 當次事件迴圈產生的新任務會立即執行

- Object.observe

- MutationObserver

- process.nextTick

看過一篇公眾號文章下面的留言:

那個所謂的mtask和task的區別我並不認同...,我認為事件對列只有一個,就是task。

特別是對於JS非同步程式設計思維還不太熟悉的同學,比如兩年前從java轉成java後的我,對於這種非同步的呼叫順序其實很難理解。

不過有一個特別能說明Macrotasks和Microtasks的例子:

上面的程式碼相信大家非常好理解,一個很簡單的遞迴,由於事件迴圈得不到釋放,UI渲染無法進行導致頁面無響應。

通常我們可以使用setTimeout來進行改造,我們把下一次執行放到非同步佇列裡面,不會持久的佔用計算資源,這就是我們說的Macrotasks:

但是Promise回撥產生的Microtasks呢,如下程式碼,同樣會造成死迴圈。

通過上文我們也可以知道當次事件迴圈產生的新Microtasks會立即執行,同時當次事件迴圈要等到所有Microtasks佇列執行完畢後才會結束。所以當我們的Microtasks在產生新的任務的同時,會導致Microtasks佇列一直有任務等待執行,這次事件迴圈永遠不會退出,也就導致了我們的死迴圈。

當然,上文解決了本人關於Microtasks的相關疑慮 (~~特別是有人拿出一段參雜setTimeout和Promise的程式碼讓你看程式碼輸出順序時~~) 的同時,也讓我回憶起似乎曾幾何時也在哪裡看到過關於Microtask的字眼。

經過多日的尋找,終於在以前寫過的一片關於Promise的總結文章 開啟Promise的正確姿勢 裡找到了。該文通過一個例項說明瞭新建Promise的程式碼是會立即執行的,並不會放到非同步佇列裡:

上面的程式碼輸出

我們得到兩點結論:

但是當時本人忽略了Promise/A+的相關註解內容:

Here “platform code” means engine,environment,and promise implementation code. In practice,this requirement ensures that onFulfilledand onRejectedexecute asynchronously,after the event loop turn in which thenis called,and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeoutor setImmediate,or with a “micro-task” mechanism such as MutationObserveror process.nextTick. Since the promise implementation is considered platform code,it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

是的,這就是本人與MicroTasks的第一次相遇,沒有一見鍾情還真是非常抱歉啊。

該註解說明瞭Promise的 onFulfilled和 onRejected回撥的執行只要確保是在 then被呼叫後非同步執行就可以了。具體實現成 setTimeout似的 macrotasks 機制或者 process.nextTick似的microtasks機制都可以,具體視平臺程式碼而定。

為什麼需要Microtasks

搜尋引擎能找到的相關文章基本都指向了一篇《Tasks,microtasks,queues and schedules》,也許這就是傳說中原罪的發源之地吧。

Microtasksare usually scheduled for things that should happen straight after the currently executing ,such as reacting to a batch of actions,or to make something async without taking the penalty of a whole new task.

簡單來說,就是希望對一系列的任務做出迴應或者執行非同步操作,但是又不想額外付出一整個非同步任務的代價。在這種情況下,Microtasks就可以用來排程這些應當在當前執行指令碼結束後立馬執行的任務

The microtask queue is processed after callbacks as long as no other Java is mid-execution,and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed.

單獨看Macrotasks和 Microtasks,執行順序可以總結如下:

從這個方面我們也可以理解為什麼Promise.then要被實現成Microtasks,回撥在實現Promise/A+規範 (必須是非同步執行)的基礎上,也保證能夠更快的被執行,而不是跟Macrotasks一樣必須等到下次事件迴圈才能執行。大家可以重新執行一下上文對比Macrotasks和Microtasks時舉的例子,也會發現他們兩的單位時間內的執行次數是不一樣的。

可以試想一些綜合了非同步任務和同步任務的的Promise例項,Microtasks可以保證它們更快的得到執行資源,例如:

如果上面的程式碼是為了載入遠端的資源,那麼只有第一次需要執行非同步載入,後面的所有執行都可以直接同步讀取快取內容。如果使用Microtasks,我們也就不用每次都等待多一次的事件迴圈來獲取該資源,Promise例項的新建過程是立即執行的,同時 onFulfilled回撥也是在本次事件迴圈中全部執行完畢的,減少了切換上下文的成本,提高了效能。

但是呢,從上文關於Promise/A+規範的引用中我們已經知道不同瀏覽器對於該實現是不一致的。部分瀏覽器 (越來越少) 將Promise的回撥函式實現成了Macrotasks,原因就在於Promise的定義來自ECMA而不是HTML。

A Job is an abstract operation that initiates an ECMA computation when no other ECMA computation is currently in progress. A Job abstract operation may be defined to accept an arbitrary set of job parameters.

按照ECMA的規範,是沒有Microtasks的相關定義的,類似的有一個 jobs的概念,和Microtasks很相似.

相關應用

Vue - src/core/utils/next-tick.js 中也有相關Macrotask和Microtask的實現

社群官網:http://imweb.io/

招聘帖:https://hr.tencent.com/position_detail.php?id=26701

IMWeb前端社群公眾號,獲取最新前端好文

微博、掘金、Github、知乎可搜尋 IMWebIMWeb團隊關注我們。

推薦您的文章

其他文章