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

本文由 IMWeb 團隊成員 shijisun 首發於 IMWeb 社區網站 imweb.io。點擊閱讀原文查看 IMWeb 社區更多精彩文章。javascript

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

Google Developer Day China 2018 by Jake Archibald

2018年9月21日,雖然沒有參加該場GDD,可是也有幸拜讀了百度@小蘑菇小哥總結的文章深刻瀏覽器的事件循環(GDD@2018),配注的說明插圖形象生動,文終的click代碼也頗有意思,推薦你們閱讀。這裏就先恬不知恥的將該文的精華以及一些本身的總結陳列以下:react

圖片

異步任務 特色 常見產生處
Tasks (Macrotasks) - 當次事件循環執行隊列內的一個任務
- 當次事件循環產生的新任務會在指定時機加入任務隊列等待執行
- setTimeout
- setInterval
- setImmediate
- I/O
Animation callbacks - 渲染過程(Structure-Layout-Paint)前執行
- 當次事件循環執行隊列裏的全部任務
- 當次事件循環產生的新任務會在下一次循環執行
- rAF
Microtasks - 當次事件循環的結尾當即執行的任務
- 當次事件循環執行隊列裏的全部任務
- 當次事件循環產生的新任務會當即執行
- Promise
- Object.observe
- MutationObserver
- process.nextTick

直觀的感覺一下 Macrotasks 和 Microtasks

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

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

特別是對於JS異步編程思惟還不太熟悉的同窗,好比兩年前從java轉成javascript後的我,對於這種異步的調用順序其實很難理解。promise

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

// 普通的遞歸, 形成死循環, 頁面無響應function callback() {    console.log('callback');    callback();}callback();

上面的代碼相信你們很是好理解,一個很簡單的遞歸,因爲事件循環得不到釋放,UI渲染沒法進行致使頁面無響應。緩存

一般咱們可使用setTimeout來進行改造,咱們把下一次執行放到異步隊列裏面,不會持久的佔用計算資源,這就是咱們說的Macrotasks:app

// Macrotasks,不會形成死循環function callback() {  console.log('callback');  setTimeout(callback,0);}callback();

可是Promise回調產生的Microtasks呢,以下代碼,一樣會形成死循環。異步

經過上文咱們也能夠知道當次事件循環產生的新Microtasks會當即執行,同時當次事件循環要等到全部Microtasks隊列執行完畢後纔會結束。因此當咱們的Microtasks在產生新的任務的同時,會致使Microtasks隊列一直有任務等待執行,此次事件循環永遠不會退出,也就致使了咱們的死循環。

// Microtasks,一樣會形成死循環,頁面無響應function callback() {  console.log('callback');  Promise.resolve().then(callback);}callback();

Microtasks 與 Promise A+

固然,上文解決了本人關於Microtasks的相關疑慮 (~~特別是有人拿出一段參雜setTimeout和Promise的代碼讓你看代碼輸出順序時~~) 的同時,也讓我回憶起彷佛曾幾什麼時候也在哪裏看到過關於Microtask的字眼。

通過多日的尋找,終於在之前寫過的一片關於Promise的總結文章 打開Promise的正確姿式 裏找到了。該文經過一個實例說明了新建Promise的代碼是會當即執行的,並不會放到異步隊列裏:

var d = new Date();// 建立一個promise實例,該實例在2秒後進入fulfilled狀態var promise1 = new Promise(function (resolve,reject) {  setTimeout(resolve,2000,'resolve from promise 1');});// 建立一個promise實例,該實例在1秒後進入fulfilled狀態var promise2 = new Promise(function (resolve,reject) {  setTimeout(resolve,1000,promise1); // resolve(promise1)});promise2.then(  result => console.log('result:',result,new Date() - d),  error => console.log('error:',error))

上面的代碼輸出

result: resolve from promise 1 2002

咱們獲得兩點結論:

  • 驗證了Promise/A+中的2.3.2規範

  • 新建Promise的代碼時會當即執行的 (運行時間是2秒而不是3秒)

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

Here 「platform code」 means engine,environment,and promise implementation code. In practice,this requirement ensures that onFulfilled and onRejected execute asynchronously,after the event loop turn in which then is 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 MutationObserver or 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的 onFulfilledonRejected 回調的執行只要確保是在 then被調用後異步執行就能夠了。具體實現成 setTimeout 似的 macrotasks 機制或者 process.nextTick 似的microtasks機制均可以,具體視平臺代碼而定。

爲何須要Microtasks

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

Microtasks are usually scheduled for things that should happen straight after the currently executing script,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 JavaScript 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,執行順序能夠總結以下:

  • 取出Macrotasks任務隊列的一個任務,執行;

  • 取出Microtasks任務隊列的全部任務,依次執行;

  • 本次事件循環結束,等待下次事件循環;

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

能夠試想一些綜合了異步任務和同步任務的的Promise實例,Microtasks能夠保證它們更快的獲得執行資源,例如:

new Promise((resolve) => {  if(/* 檢查資源是否須要異步加載 */) {    return asyncAction().then(resolve);  }  // 直接返回加載好的異步資源  return syncResource;});

若是上面的代碼是爲了加載遠程的資源,那麼只有第一次須要執行異步加載,後面的全部執行均可以直接同步讀取緩存內容。若是使用Microtasks,咱們也就不用每次都等待多一次的事件循環來獲取該資源,Promise實例的新建過程是當即執行的,同時 onFulfilled回調也是在本次事件循環中所有執行完畢的,減小了切換上下文的成本,提升了性能。

可是呢,從上文關於Promise/A+規範的引用中咱們已經知道不一樣瀏覽器對於該實現是不一致的。部分瀏覽器 (愈來愈少) 將Promise的回調函數實現成了Macrotasks,緣由就在於Promise的定義來自ECMAScript而不是HTML。

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

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

相關應用

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

let microTimerFunclet macroTimerFuncif (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {  macroTimerFunc = () => {    setImmediate(flushCallbacks)  }} else if (typeof MessageChannel !== 'undefined' && (  isNative(MessageChannel) ||  // PhantomJS  MessageChannel.toString() === '[object MessageChannelConstructor]')) {  const channel = new MessageChannel()  const port = channel.port2  channel.port1.onmessage = flushCallbacks  macroTimerFunc = () => {    port.postMessage(1)  }} else {  /* istanbul ignore next */  macroTimerFunc = () => {    setTimeout(flushCallbacks,0)  }}// Determine microtask defer implementation./* istanbul ignore next,$flow-disable-line */if (typeof Promise !== 'undefined' && isNative(Promise)) {  const p = Promise.resolve()  microTimerFunc = () => {    p.then(flushCallbacks)    // in problematic UIWebViews,Promise.then doesn't completely break,but    // it can get stuck in a weird state where callbacks are pushed into the    // microtask queue but the queue isn't being flushed,until the browser    // needs to do some other work,e.g. handle a timer. Therefore we can    // "force" the microtask queue to be flushed by adding an empty timer.    if (isIOS) setTimeout(noop)  }} else {  // fallback to macro  microTimerFunc = macroTimerFunc}
相關文章
相關標籤/搜索