深入了解 JavaScript 的 Promise 概念與應用

更新日期: 2024 年 11 月 8 日

JavaScript 的 Promise 是一種用來處理非同步操作的物件,提供了一種優雅的方式來管理異步任務的執行狀態和結果。

在這篇文章中,我們將詳細解釋 Promise 的各個層面,包括狀態、方法和搭配 async/await 的使用方式。

什麼是 Promise

Promise 是 JavaScript 的內建物件之一,主要用來處理異步任務,提供了一個更方便的方式來控制非同步操作的流程。Promise 本身是一個函數,可以使用 new 關鍵字來創建。創建一個 Promise 時,會傳入一個執行函數,此執行函數接收兩個參數:okfail

這兩個參數本身也是函數,分別代表了 Promise 成功或失敗時的處理結果:

  • ok:當任務成功完成時會呼叫,用來將 Promise 狀態設為「成功」(Fulfilled)。
  • fail:當任務失敗時會呼叫,用來將 Promise 狀態設為「失敗」(Rejected)。

例如,下面是創建一個 Promise 的範例:

const p1 = new Promise((ok, fail) => {
  ok(123); // 執行成功,將狀態設為 Fulfilled,並返回 123
});

console.log(p1); // 會輸出 Promise {<fulfilled>: 123}

在這段程式碼中,p1 是一個新的 Promise 物件,並執行 ok(123),表示任務已成功完成,並將 Promise 狀態設為 Fulfilled,最終返回 123 作為結果。

Promise 的三種狀態

每個 Promise 都有三個主要狀態:

  1. Pending(待定):Promise 正在等待執行結果,尚未結束。
  2. Fulfilled(已成功):Promise 成功執行並返回結果。
  3. Rejected(已失敗):Promise 執行失敗並返回錯誤。

當我們在一個新創建的 Promise 中什麼都不做時,狀態會維持在 Pending。例如:

const p1 = new Promise((ok, fail) => {
  // 未執行任何操作
});

console.log(p1); // 顯示 Promise {<pending>}

resolvereject 函數

在 JavaScript 中,Promise 常使用 resolvereject 作為參數來處理成功或失敗的狀態。

這些名稱是開發中約定俗成的寫法,因為它們能明確地表示狀態的含義,提高程式的可讀性。

使用 resolvereject 處理非同步操作

通常,我們會將 ok 函數替換為 resolve,將 fail 函數替換為 rejectresolve 用來表示任務成功完成,而 reject 則表示任務失敗。以下範例展示了一個 Promise 使用 resolve 的情況:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("ok"); // 執行成功並返回 "ok"
  }, 3000);
});

p1.then((result) => {
  console.log(result); // 顯示 "ok"
});

在這段程式碼中,resolve("ok") 在 3 秒後被執行,將 Promise 的狀態設為成功,並將結果 "ok" 傳遞給 .then() 中的回呼函數,最終輸出 "ok"

使用 reject 處理失敗情況

當 Promise 需要在非同步操作中處理錯誤時,可以呼叫 reject 來改變狀態。以下範例展示了如何用 reject 處理錯誤:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("ng"); // 執行失敗並傳回錯誤訊息 "ng"
  });
});

p1.then((result) => {
  console.log(result);
});

這段程式碼會在 3 秒後執行 reject("ng"),使 Promise 狀態變成失敗,並傳回錯誤訊息 "ng"。然而,若未使用 .catch() 來處理錯誤,瀏覽器會顯示錯誤訊息:

Uncaught (in promise) ng

使用 .catch() 來捕捉錯誤

為了避免系統錯誤訊息,我們可以用 .catch() 來捕捉 Promise 中的錯誤,並自訂錯誤處理方式:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("ng"); // 3 秒後執行失敗並傳回 "ng"
  });
});

p1.then((result) => {
  console.log(result);
}).catch((error) => {
  console.log(error); // 顯示 "ng" 作為一般文字,而非系統錯誤訊息
});

透過 .catch() 來處理錯誤訊息,Promise 的錯誤可以被安全地捕捉並顯示,不會出現系統的錯誤紅字。

resolve 的範例:成功完成的 Promise

當 Promise 成功執行時,resolve 會將狀態設為 Fulfilled,並將資料傳遞給 .then() 的回呼函數。以下範例顯示 resolve 的使用:

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok"); // 任務成功,返回結果 "ok"
  });
});

p1.then((result) => {
  console.log(result); // 顯示 "ok"
});

這段程式碼會在 3 秒後執行 resolve("ok"),設定 Promise 狀態為成功,並將結果 "ok" 輸出到控制台。

搭配 await 使用 Promise

Promise 與 async/await 是常見的組合,能使非同步程式碼更易於閱讀。例子如下:

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok");
  }, 3000);
});

console.log(1);
const r1 = await p1;
console.log(2);
console.log(r1);
console.log(3);

使用 await 的效果

若缺少 awaitp1 的狀態將保持在 Pending,因此結果會顯示:

1
2
Promise {<pending>}
3

有了 await,則會等待 p1 完成後再顯示結果。此例程式碼會顯示:

1
2
ok
3

多個 Promise 的執行順序與 await 的應用

在 JavaScript 中,當有多個 Promise 同時執行時,可以使用 await 關鍵字依序處理它們的結果。

透過 await,我們可以控制異步任務的執行順序,但必須等待前一個 Promise 完成後才會繼續執行下一個。

這種方式能讓異步程式碼,像同步程式碼一樣清晰易讀。

基本範例:單個 Promise 搭配 await

以下範例展示了如何在單個 Promise 上使用 await

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok");
  }, 3000);
});

console.log(1);
const r1 = await p1;
console.log(2);
console.log(r1);
console.log(3);

在這段程式碼中:

  1. 先顯示 1
  2. 進入 await p1 等待 p1 完成
  3. p1 在 3 秒後回傳 "ok"
  4. 完成後依序顯示 2 -> ok -> 3

若省略 await,結果將立即顯示 1 -> 2 -> Promise {<pending>} -> 3,因為 p1 的狀態為 Pending,並未等待其完成。

多個 Promise 搭配 await 的執行順序

當有多個 Promise 需要依序執行,await 會等待上一個 Promise 完成後才進入下一個。

以下範例演示了多個 Promise 之間的執行順序:

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok");
  }, 6000); // 6 秒後完成
});

const p2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok");
  }, 3000); // 3 秒後完成
});

const r1 = await p1;
console.log(r1); // 等待 6 秒後顯示 "ok"

const r2 = await p2;
console.log(r2); // 隨即顯示 "ok"

結果解釋

由於 await p1 必須等待 p1 完成,整段程式碼需要等 p1 的 6 秒倒數結束,之後才執行 p2 的等待(3 秒)。因此,結果顯示順序為:

  1. 等待 6 秒後顯示 r1"ok"
  2. 接著等待 p2 的 3 秒,然後顯示 r2"ok"

提前開始計時的特性

Promise 一旦創建即開始倒數計時,即使程式尚未進行 await 操作。

以下例子說明這一特性:

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok");
  }, 6000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve("ok");
  }, 3000);
});

const r1 = await p1;
console.log(r1);

const r2 = await p2;
console.log(r2);

解答與說明

  1. 接著 r2 立刻顯示 ok
  2. p1p2 一創建就開始倒數(分別為 6 秒和 3 秒)
  3. p2 3 秒後完成,等待 p1 的 6 秒
  4. 6 秒過後,r1 顯示 ok

async 函數的特性

async 函數是一種特殊的函數,用來自動封裝並返回一個狀態為 Fulfilled 的 Promise。這樣一來,我們可以直接使用 await.then() 來處理該函數的回傳結果,而無需手動建立 Promise。

async 函數如何自動回傳 Promise

async 函數被呼叫時,它會自動將回傳的值封裝成一個 Fulfilled 的 Promise,即使我們只回傳一個單純的數值。例如:

async function h1() {
  return 123;
}

const result = h1();
console.log(result); // 輸出:Promise {<fulfilled>: 123}

在這段程式碼中,h1 函數回傳 123,但由於它是 async 函數,因此會自動封裝成 Promise {<fulfilled>: 123}

使用 await 獲取 async 函數的回傳值

我們可以在 async 函數的外部用 await 關鍵字來直接取得其回傳值,而不需要再手動調用 .then()

async function h1() {
  return 123;
}

const result = await h1();
console.log(result); // 輸出:123

在這裡,await 等待 h1 函數完成,並直接取得回傳值 123,不需要額外封裝或調用 .then()

使用 .then() 處理 async 函數的結果

如果不使用 await 而選擇 .then(),那麼 async 函數的結果將會是另一個 Promise,需要進一步解析。例如:

async function h1() {
  return 123;
}

const result = h1().then((value) => {
  console.log(value); // 輸出:123
});

在這裡,.then() 回呼函數會取得 123 作為 value,並將其輸出至控制台。

小結

async 函數的特性讓非同步處理更加簡潔,我們可以選擇使用 await.then() 來處理其回傳結果,視需求靈活使用這兩種方式。

結論

Promise 為 JavaScript 的非同步程式設計提供了強大而靈活的功能。

透過不同的狀態(Pending、Fulfilled、Rejected),搭配 .then().catch() 來捕捉結果與錯誤,再加上 async/await 的應用,可以使程式碼更加清晰易讀。

在多個非同步操作的情境下,善用 await 關鍵字能幫助我們更有效地控制執行順序和時間。

Similar Posts