深入了解 JavaScript 的 Promise 概念與應用
更新日期: 2024 年 11 月 8 日
JavaScript 的 Promise 是一種用來處理非同步操作的物件,提供了一種優雅的方式來管理異步任務的執行狀態和結果。
在這篇文章中,我們將詳細解釋 Promise 的各個層面,包括狀態、方法和搭配 async/await
的使用方式。
什麼是 Promise
Promise 是 JavaScript 的內建物件之一,主要用來處理異步任務,提供了一個更方便的方式來控制非同步操作的流程。Promise 本身是一個函數,可以使用 new
關鍵字來創建。創建一個 Promise 時,會傳入一個執行函數,此執行函數接收兩個參數:ok
和 fail
。
這兩個參數本身也是函數,分別代表了 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 都有三個主要狀態:
- Pending(待定):Promise 正在等待執行結果,尚未結束。
- Fulfilled(已成功):Promise 成功執行並返回結果。
- Rejected(已失敗):Promise 執行失敗並返回錯誤。
當我們在一個新創建的 Promise 中什麼都不做時,狀態會維持在 Pending。例如:
const p1 = new Promise((ok, fail) => {
// 未執行任何操作
});
console.log(p1); // 顯示 Promise {<pending>}
resolve
和 reject
函數
在 JavaScript 中,Promise 常使用 resolve
和 reject
作為參數來處理成功或失敗的狀態。
這些名稱是開發中約定俗成的寫法,因為它們能明確地表示狀態的含義,提高程式的可讀性。
使用 resolve
和 reject
處理非同步操作
通常,我們會將 ok
函數替換為 resolve
,將 fail
函數替換為 reject
。resolve
用來表示任務成功完成,而 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
的效果
若缺少 await
,p1
的狀態將保持在 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
- 進入
await p1
等待p1
完成 p1
在 3 秒後回傳"ok"
- 完成後依序顯示
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 秒)。因此,結果顯示順序為:
- 等待 6 秒後顯示
r1
的"ok"
- 接著等待
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);
解答與說明
- 接著
r2
立刻顯示ok
p1
和p2
一創建就開始倒數(分別為 6 秒和 3 秒)p2
3 秒後完成,等待p1
的 6 秒- 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
關鍵字能幫助我們更有效地控制執行順序和時間。