你可能不瞭解的 Promise 微任務型別

deepfunc發表於2022-05-28

一道示例題引發的知識盲區

// promise1
new Promise(resolve => {
  resolve(
    new Promise(resolve => {
      resolve(1);
    })
  );
}).then(res => {
  console.log('1');
});

new Promise(resolve => {
  resolve(2);
})
  .then(() => {
    console.log('2');
  })
  .then(() => {
    console.log('3');
  })
  .then(() => {
    console.log('4');
  });

// 輸出順序:
// 2
// 3
// 1
// 4

先來看一道示例題。按照以往的理解,我以為輸出順序是 2 1 3 4。然後通過除錯發現 promise1 在初始化後狀態依然是 pending,感覺自己在理解 Promise 微任務方面還是存在不足。研究了下 ECMA 規範,終於把這個問題搞清楚了。

微任務的型別

ECMA 規範中把 Promise 微任務分成了兩種型別,下面結合規範來分別看下這兩種微任務的產生時機和執行內容。

NewPromiseReactionJob

當 Promise 決議後執行 then() 中註冊的回撥時,或當 then() 註冊時 Promise 已決議,會產生這種微任務。為簡化說明,我們先關注這個場景:當前 Promise 已經決議,接著呼叫了 then(),比如這樣:

Promise.resolve(1).then(res => console.log(res));

res => console.log(res) 就執行在這種微任務中。來看規範對於 Promise.prototype.then 的描述:

因為 Promise 已經是 fulfilled 狀態,我們接著看 PerformPromiseThen 中對於 fulfilled 狀態的操作:

這裡就是建立了一個 NewPromiseReactionJob 微任務,並加入到了微任務佇列中。我們再看看 NewPromiseReactionJob 裡面是怎麼執行的:

該微任務主要包含兩個內容:

  1. 執行 handler,handler 就是 then() 中註冊的回撥,得到返回結果。
  2. 對 then() 中產生的新 Promise 執行 resolve(返回結果) 或 reject(返回結果)。

NewPromiseResolveThenableJob

上面那種微任務基本是大家熟知的情況,這個微任務型別就是示例題中提到的盲區了。首先注意到 resolve 函式的描述:

如果一個物件的 then 屬性可以被呼叫(是一個函式),那麼這個物件就是 thenable 物件。呼叫 resolve() 傳遞的引數值如果是一個 thenable 物件,就會產生 NewPromiseResolveThenableJob 這種微任務了。接下來看看這個微任務的內容:

大概意思就是這種微任務產生了如下的程式碼:

// resovle 和 reject 是呼叫 resolve(thenable) 時那個 Promise 上的。
thenable.then(resolve, reject); 

那麼結合第一種微任務,如果 thenable 物件是 Promise,則這個微任務執行後又會產生第一個微任務。為什麼要這樣做呢?規範上有一段解釋:

This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.

直接翻譯的話大概就是說要等周圍的同步程式碼執行完後才會執行這個。關於這個設計意圖,我的理解是考慮到 thenable 物件的不一定是 Promise 例項,也可能是使用者建立的任何物件;如果這個物件的 then 是同步方法,那麼這樣做就可以保證 then 的執行順序也是在微任務中。

示例題分析

我們再來分析一下示例題:

// promise1
new Promise(resolve => {
  resolve(
    // promise2
    new Promise(resolve => {
      resolve(1);
    })
  );
}).then(res => {
  console.log('1');
});

// promise3
new Promise(resolve => {
  resolve(2);
})
  .then(() => {
    console.log('2');
  })
  .then(() => {
    console.log('3');
  })
  .then(() => {
    console.log('4');
  });

程式碼執行後,我們用虛擬碼來表示下微任務佇列的內容:

const microTasks = [
  function job1() {
    promise2.then(promise1.[[Resolve]], promise1.[[Reject]]);
  },
  function job2() {
    const handler = () => {
      console.log('2');
    };
    
    // 決議 then() 返回的新 Promise。
    resolve(handler(2));
  }
];

接著開始執行微任務佇列。job 1 執行後,產生了新的微任務 job 3:

const microTasks = [
  function job2() {
    const handler = () => {
      console.log('2');
    };
    resolve(handler(2));
  },
  function job3() {
    const handler = promise1.[[Resolve]];
    resolve(handler(1));
  }
];

job 2 執行後,輸出了 2,並且產生新的微任務 job 4:

const microTasks = [
  function job3() {
    const handler = promise1.[[Resolve]];
    resolve(handler(1));
  },
  function job4() {
    const handler = () => {
      console.log('3');
    };
    resolve(handler(undefined));
  }
];

注意到 job 3 的內容是會讓 promise1 決議,那麼就會執行 promise1 的 then 回撥,則會再產生一個微任務 job 5;並且 job 4 執行完後輸出變為 2 3,並讓 then() 產生的新 Promise 決議,也會再產生下一個的微任務 job 6:

const microTasks = [
  // job 5 由 job 3 產生。
  function job5() {
    const handler = () => {
      console.log('1');
    };
    resolve(handler(1));
  },
  function job6() {
    const handler = () => {
      console.log('4');
    };
    resolve(handler(undefined));
  }
];

那麼最後的輸出結果就是 2 3 1 4 啦,大家可以把以上分析方法放在其他的題目中驗證康康是不是對的。

我的 JS 部落格:小聲比比 JavaScript

參考資料

相關文章