Promise 你真的用明白了麼?

y_ck發表於2020-09-08

文章首發自筆者的 Github

Promise 關於 API 這塊大家應該都能熟練使用,但是和微任務相關的你可能還存在知識盲區。

前置知識

在開始正文前,我們先把本文涉及到的一些內容提前定個基調。

Promise 哪些 API 涉及了微任務?

Promise 中只有涉及到狀態變更後才需要被執行的回撥才算是微任務,比如說 thencatchfinally ,其他所有的程式碼執行都是巨集任務(同步執行)。

上圖中藍色為同步執行,黃色為非同步執行(丟到微任務佇列中)。

這些微任務何時被加入微任務佇列?

這個問題我們根據 ecma 規範來看:

  • 如果此時 Promise 狀態為 pending,那麼成功或失敗的回撥會分別被加入至 [[PromiseFulfillReactions]][[PromiseRejectReactions]] 中。如果你看過手寫 Promise 的程式碼的話,應該能發現有兩個陣列儲存這些回撥函式。
  • 如果此時 Promise 狀態為非 pending 時,回撥會成為 Promise Jobs,也就是微任務。

瞭解完以上知識後,正片開始。

同一個 then,不同的微任務執行

初級

Promise.resolve()
  .then(() => {
    console.log("then1");
    Promise.resolve().then(() => {
      console.log("then1-1");
    });
  })
  .then(() => {
    console.log("then2");
  });

以上程式碼大家應該都能得出正確的答案:then1 → then1-1 → then2

雖然 then 是同步執行,並且狀態也已經變更。但這並不代表每次遇到 then 時我們都需要把它的回撥丟入微任務佇列中,而是等待 then 的回撥執行完畢後再根據情況執行對應操作。

基於此,我們可以得出第一個結論:鏈式呼叫中,只有前一個 then 的回撥執行完畢後,跟著的 then 中的回撥才會被加入至微任務佇列。

中級

大家都知道了 Promise resolve 後,跟著的 then 中的回撥會馬上進入微任務佇列。

那麼以下程式碼你認為的輸出會是什麼?

let p = Promise.resolve();

p.then(() => {
  console.log("then1");
  Promise.resolve().then(() => {
    console.log("then1-1");
  });
}).then(() => {
  console.log("then1-2");
});

p.then(() => {
  console.log("then2");
}); 

按照一開始的認知我們不難得出 then2 會在 then1-1 後輸出,但是實際情況卻是相反的。

基於此我們得出第二個結論:每個鏈式呼叫的開端會首先依次進入微任務佇列。

接下來我們換個寫法:

let p = Promise.resolve().then(() => {
  console.log("then1");
  Promise.resolve().then(() => {
    console.log("then1-1");
  });
}).then(() => {
  console.log("then2");
});

p.then(() => {
  console.log("then3");
});

上述程式碼其實有個陷阱,then 每次都會返回一個新的 Promise,此時的 p 已經不是 Promise.resolve() 生成的,而是最後一個 then 生成的,因此 then3 應該是在 then2 後列印出來的。

順便我們也可以把之前得出的結論優化為:同一個 Promise 的每個鏈式呼叫的開端會首先依次進入微任務佇列。

高階

以下大家可以猜猜 then1-2 會在何時列印出來?

Promise.resolve()
  .then(() => {
    console.log("then1");
    Promise.resolve()
      .then(() => {
        console.log("then1-1");
        return 1;
      })
      .then(() => {
        console.log("then1-2");
      });
  })
  .then(() => {
    console.log("then2");
  })
  .then(() => {
    console.log("then3");
  })
  .then(() => {
    console.log("then4");
  });

這題肯定是簡單的,記住第一個結論就能得出答案,以下是解析:

  • 第一次 resolve 後第一個 then 的回撥進入微任務佇列並執行,列印 then1
  • 第二次 resolve 後內部第一個 then 的回撥進入微任務佇列,此時外部第一個 then 的回撥全部執行完畢,需要將外部的第二個 then 回撥也插入微任務佇列。
  • 執行微任務,列印 then1-1then2,然後分別再將之後 then 中的回撥插入微任務佇列
  • 執行微任務,列印 then1-2then3 ,之後的內容就不一一說明了

接下來我們把 return 1 修改一下,結果可就大不相同啦:

Promise.resolve()
  .then(() => {
    console.log("then1");
    Promise.resolve()
      .then(() => {
        console.log("then1-1");
        return Promise.resolve();
      })
      .then(() => {
        console.log("then1-2");
      });
  })
  .then(() => {
    console.log("then2");
  })
  .then(() => {
    console.log("then3");
  })
  .then(() => {
    console.log("then4");
  });

當我們 return Promise.resolve() 時,你猜猜 then1-2 會何時列印了?

答案是最後一個才被列印出來。

為什麼在 then 中分別 return 不同的東西,微任務的執行順序竟有如此大的變化?以下是筆者的解析。

PS:then 返回一個新的 Promise,並且會用這個 Promise 去 resolve 返回值,這個概念需要大家先了解一下。

根據 Promise A+ 規範

根據規範 2.3.2,如果 resolve 了一個 Promise,需要為其加上一個 thenresolve

if (x instanceof MyPromise) {
  if (x.currentState === PENDING) {
  } else {
    x.then(resolve, reject);
  }
  return;
}

上述程式碼節選自手寫 Promise 實現。

那麼根據 A+ 規範來說,如果我們在 then 中返回了 Promise.resolve 的話會多入隊一次微任務,但是這個結論還是與實際不符的,因此我們還需要尋找其他權威的文件。

根據 ECMA - 262 規範

根據規範 25.6.1.3.2,當 Promise resolve 了一個 Promise 時,會產生一個NewPromiseResolveThenableJob,這是屬於 Promise Jobs 中的一種,也就是微任務。

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.

並且該 Jobs 還會呼叫一次 then 函式來 resolve Promise,這也就又生成了一次微任務。

這就是為什麼會觸發兩次微任務的來源。

最後

文章到這裡就完結了,大家有什麼疑問都可以在評論區提出。

推薦關注我的微信公眾號【前端真好玩】,工作日推送高質量文章。

image.png

筆者就職於酷家樂,裝潢設計行業獨角獸。一流的視覺化、前端技術團隊,有興趣的可以簡歷投遞至 zx597813039@gmail.com

相關文章