Pr

Promise 在 JavaScript

1 個練習

關於 Promise

Promise物件代表一個非同步操作最終的完成(或失敗),以及它所產生的值。

Note

對很多人來說,這是個艱深的主題,尤其是當你熟悉的程式語言是完全_同步_的時候。 如果你覺得難以招架,或者想進一步了解並行與平行,可以觀看(透過 go.dev)或直接透過 vimeo 觀看,也可以閱讀投影片,這是場精彩的演講「Concurrency is not parallelism」。

Promise 的生命週期

Promise有三種狀態:

  1. 待處理
  2. 已實現
  3. 已拒絕

當它被建立時,Promise 處於待處理狀態。 在未來的某個時間點,它可能會_解決_或_拒絕_。 一旦 Promise 被解決或拒絕過一次,它就永遠無法再次被解決或拒絕,其狀態也無法改變。

換句話說:

  1. 處於待處理狀態時,Promise:
    • 可以轉換為已實現或已拒絕狀態。
  2. 處於已實現狀態時,Promise:
    • 不得轉換為任何其他狀態。
    • 必須有一個值,且該值不得改變。
  3. 處於已拒絕狀態時,Promise:
    • 不得轉換為任何其他狀態。
    • 必須有一個原因,且該原因不得改變。

解決 Promise

Promise 可以用多種方式解決:

// Creates a promise that is immediately resolved
Promise.resolve(value);

// Creates a promise that is immediately resolved
new Promise((resolve) => {
  resolve(value);
});

// Chaining a promise leads to a resolved promise
somePromise.then(() => {
  // ...
  return value;
});

在上面的範例中,value可以是_任何東西_,包括錯誤、undefined、null或另一個 Promise。 通常你會想用一個不是錯誤的值來解決它。

拒絕 Promise

Promise 可以用多種方式拒絕:

// Creates a promise that is immediately rejected
Promise.reject(reason)

// Creates a promise that is immediately rejected
new Promise((_, reject) {
  reject(reason)
})

// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
  // ...
  throw reason
})

在上面的範例中,reason可以是_任何東西_,包括錯誤、undefined或null。 通常你會想用一個錯誤來拒絕它。

串接 Promise

Promise 一旦解決或拒絕,就可以用後續的動作_延續_下去。

then

每個 Promise 都是「thenable」。 這表示會有一個then函式可以使用,它會在原始的 Promise 解決後被執行。 給定promise.then(onResolved),回呼onResolved會收到原始 Promise 被解決時的值。 這永遠會回傳一個_新的_「串接」Promise。

從then回傳一個value會解決「串接」的 Promise。 在then中拋出一個reason會拒絕「串接」的 Promise。

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

const promise2 = promise1.then(function (value) {
  console.log(value);
  // expected output: "Success!"

  return true;
});

這會在大約 1000 毫秒後記錄"Success!"。 promise1的狀態與值會是resolved和"Success!"。 promise2的狀態與值會是resolved和true。

還有第二個引數可以使用,它會在原始 Promise 拒絕時執行。 給定promise.then(onResolved, onRejected),回呼onResolved會收到原始 Promise 被解決時的值,或者回呼onRejected會收到 Promise 被拒絕的原因。

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return true;
}

function shout(reason) {
  console.error(reason.toUpperCase());
  return false;
}

const promise2 = promise1.then(log, shout);
  • 在大約 1/2 的情況下,這會在大約 1000 毫秒後記錄"Success!"。
    • promise1的狀態與值會是resolved和"Success!"。
    • promise2的狀態與值會是resolved和true。
  • 在大約 1/2 的情況下,這會立即記錄"NOPE!"。
    • promise1的狀態與值會是rejected和Nope!。
    • promise2的狀態與值會是resolved和false。

重要的是要了解,由於生命週期的規則,當它reject時,約 1000 毫秒後才傳進來的resolve會被默默忽略,因為內部狀態一旦被拒絕或解決就無法再改變。 重要的是要了解,從 Promise 回傳一個值會解決它,而拋出一個值則會拒絕它。 當promise1解決且有一個串接的onResolved:then(onResolved)時,那個後續動作就是一個新的 Promise,可以解決或拒絕。 當promise1拒絕但有一個串接的onRejected:then(, onRejected)時,那個後續動作就是一個新的 Promise,可以解決或拒絕。

catch

有時候你會想捕捉錯誤,並只在原始 Promise reject時繼續。 給定promise.catch(onCatch),回呼onCatch會收到原始 Promise 被拒絕的原因。 這永遠會回傳一個_新的_「串接」Promise。

從catch回傳一個value會解決「串接」的 Promise。 在catch中拋出一個reason會拒絕「串接」的 Promise。

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return 'done';
}

function recover(reason) {
  console.error(reason.toUpperCase());
  return 42;
}

const promise2 = promise1.catch(recover).then(log);

在大約 1/2 的情況下,這會在大約 1000 毫秒後記錄"Success!"。 在另外 1/2 的情況下,這會立即記錄42。

  • 如果promise1解決,catch會被略過,接著會到達then,並記錄該值。
    • promise1的狀態與值會是resolved和"Success!"。
    • promise2的狀態與值會是resolved和"done";
  • 如果promise1拒絕,catch會被執行,它會_回傳一個值_,因此整個串接鏈現在變成resolved,接著會到達then,並記錄該值。
    • promise1的狀態與值會是rejected和"Nope!"。
    • promise2的狀態與值會是resolved和"done";

finally

有時候你會想在 Promise 有結果之後執行程式碼,無論該 Promise 是解決還是拒絕。 給定promise.finally(onSettled),回呼onSettled不會收到任何東西。 這永遠會回傳一個_新的_「串接」Promise。

從finally回傳一個value會複製原始 Promise 的狀態與值,並忽略該value。 在finally中拋出一個reason會拒絕「串接」的 Promise,並覆寫原始 Promise 的任何狀態與值或原因。

範例

把各種方法組合在一起:

const myPromise = new Promise(function (resolve, reject) {
  const sampleData = [2, 4, 6, 8];
  const randomNumber = Math.round(Math.random() * 5);

  if (sampleData[randomNumber]) {
    resolve(sampleData[randomNumber]);
  } else {
    reject('Sampling did not result in a sample');
  }
});

const finalPromise = myPromise
  .then(function (sampled) {
    // If the random number was 0, 1, 2, or 3, this will be
    // reached and the number 2, 4, 6, or 8 will be logged.
    console.log(`Sampled data: ${sampled}`);
    return 'yay';
  })
  .catch(function (reason) {
    // If the random number was 4 or 5, this will be reached and
    // reason will be "An error occurred". The entire chain will
    // then reject with an Error with the reason as message.
    throw new Error(reason);
  })
  .finally(function () {
    // This will always log after either the sampled data is
    // logged or the error is raised.
    console.log('Promise completed');
  });
  • 在randomNumber為0-3的情況:
    • myPromise會以值2, 4, 6, or 8被解決
    • finalPromise會以值'yay'被解決
    • 會有兩筆記錄:
      • Sampled data: ...
      • Promise completed
  • 在randomNumber為4-5的情況:
    • myPromise會以原因'Sampling did not result in a sample'被拒絕
    • finalPromise會以原因Error('Sampling did not result in a sample')被拒絕
    • 會有一筆記錄:
      • Promise completed
      • 在某些環境中,這會產生一筆"uncaught rejected promise: Error('Sampling did not result in a sample')"記錄

如上所示,reject可以搭配字串運作,而 Promise 也可以用Error來拒絕。

Note

如果串接 Promise 或一般用法還不清楚,MDN 上的教學是很好的參考資源。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 Promise