初學者指南:了解 JavaScript 的 Promise 物件
更新日期: 2024 年 9 月 14 日
在學習 JavaScript 的過程中,你可能會遇到一些需要處理非同步操作的情況,比如從伺服器獲取資料或等待一段時間後執行某些動作。
這些情況下,我們就需要使用 Promise 物件來處理。
同步與非同步
在深入了解 Promise 之前,先來搞清楚什麼是同步(synchronous)與非同步(asynchronous)操作。這兩個概念是理解 Promise 的關鍵。
同步操作
同步操作指的是在執行某個任務時,必須等待該任務完成,然後才能繼續執行後續任務。也就是說,任務是依次排隊執行的。
console.log("任務1");
console.log("任務2");
console.log("任務3");
在上述代碼中,任務 1 、任務 2 和任務 3 會依次執行,直到上一個任務完成,下一個任務才會開始。
非同步操作
非同步操作則允許我們在執行某個任務時,不必等待該任務完成,就可以開始執行後續任務。
這樣可以提高效率,尤其在處理需要等待的操作(例如網絡請求或計時器)時。
console.log("任務1");
setTimeout(() => {
console.log("任務2");
}, 2000);
console.log("任務3");
在上述代碼中,setTimeout 是一個非同步函數。
當執行到 setTimeout 時,JavaScript 引擎不會等待它完成,而是立即繼續執行後面的 console.log("任務3")
。
兩秒(2000 毫秒)後,非同步操作才會執行並打印出 “任務2″。
補充:setTimeout 函數介紹
setTimeout 是可以讓你在指定的時間後,執行某段程式碼的函數。
它包含兩個參數:一個是你想要執行的程式碼,另一個是延遲的時間(單位是毫秒)。
為什麼需要非同步操作?
在許多情況下,非同步操作是必要的。例如:
- 網絡請求:從伺服器獲取數據時,可能需要幾秒鐘甚至更長時間,非同步操作允許我們在等待數據的同時繼續執行其他代碼。
- 計時器:如
setTimeout
和setInterval
,允許我們在指定時間後執行某些操作,而不阻塞代碼的執行。 - 文件操作:讀寫文件可能需要時間,非同步操作允許我們在等待文件操作完成的同時繼續執行其他任務。
什麼是 Promise?
Promise 是一種用來處理非同步操作的 JavaScript 物件。它允許你寫出更具可讀性的代碼。
簡單來說,Promise 代表了一個尚未完成,但最終會完成(或失敗)的操作。
Promise 的三種狀態
- Pending(待定): 初始狀態,操作尚未完成或失敗。
- Fulfilled(完成): 操作成功完成。
- Rejected(拒絕): 操作失敗。
當 Promise 狀態從 Pending 轉為 Fulfilled 或 Rejected 時,它會呼叫相應的回調函數來處理結果。
如何創建 Promise?
我們可以通過 new Promise 來創建一個 Promise 物件。
它接受一個函數,該函數有兩個參數:resolve 和 reject。resolve 用於操作成功時呼叫,而 reject 用於操作失敗時呼叫。
const myPromise = new Promise((resolve, reject) => {
// 模擬非同步操作
setTimeout(() => {
const success = true; // 模擬操作成功或失敗
if (success) {
resolve("操作成功!");
} else {
reject("操作失敗!");
}
}, 2000);
});
我們可以使用 .then() 和 .catch() 來處理 Promise 的結果。
.then 方法
Promise.prototype.then 方法是 Promise 物件的一部分,它允許我們鏈式處理非同步操作的結果,使代碼更具可讀性和可維護性。
then 方法的語法
promise.then(onFulfilled, onRejected);
- onFulfilled(可選):當 Promise 轉為 Fulfilled(已成功)狀態時要執行的回調函數。
- onRejected(可選):當 Promise 轉為 Rejected(已失敗)狀態時要執行的回調函數。
這兩個參數都是可選的,並且它們可以返回一個值或一個新的 Promise。
使用 .then 來處理成功的結果
我們使用 .then 方法來處理這個 Promise 的成功結果:
myPromise.then((successMessage) => {
console.log(successMessage); // 打印 "操作成功!"
});
處理失敗的結果
為了處理失敗的情況,我們可以在 .then 方法中傳入第二個回調函數,或者使用 .catch 方法:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("操作失敗!");
}, 1000);
});
myPromise
.then(
(successMessage) => {
console.log(successMessage);
},
(errorMessage) => {
console.error(errorMessage); // 打印 "操作失敗!"
}
);
// 或者使用 .catch
myPromise
.then((successMessage) => {
console.log(successMessage);
})
.catch((errorMessage) => {
console.error(errorMessage); // 打印 "操作失敗!"
});
在這個示例中,每個 .then
方法返回一個新的 Promise,並將結果傳遞給下一個 .then
。這樣可以依次處理多個非同步操作,並確保它們按順序執行。
錯誤處理
.then
方法中的任何錯誤都會傳遞給下一個 .catch
方法來處理:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("操作成功!");
}, 1000);
});
myPromise
.then((successMessage) => {
console.log(successMessage);
throw new Error("出現錯誤");
})
.then((message) => {
console.log(message); // 不會執行
})
.catch((error) => {
console.error(error); // 打印 "出現錯誤"
});
總結
Promise.prototype.then
方法是處理 JavaScript 非同步操作的核心工具之一。通過理解和掌握 .then
方法的用法,你可以寫出更具可讀性和可維護性的代碼,並能夠有效地處理一系列的非同步操作。希望這篇文章能幫助你更好地理解 .then
方法的使用。
回調地獄(Callback Hell)
什麼是回調?
回調(callback)是指將一個函數作為參數傳遞給另一個函數,並在某個時間點調用這個參數函數。這種模式在處理非同步操作時非常常見。
什麼是回調地獄?
當需要執行一系列的非同步操作時,回調函數就會嵌套在一起,變得難以閱讀和維護,這種情況被稱為「回調地獄」。
舉例來說,若有任務 1、任務 2 、任務 3 三個代碼,後者需要仰賴前者執行完成,才能繼續執行,這導致了回調函數的多層嵌套,代碼變得難以維護和閱讀。
看下面的例子:
// 定義第一個任務,接收一個回調函數作為參數
function firstTask(callback) {
// 使用 setTimeout 函數,延遲 1 秒後執行
setTimeout(() => {
console.log("完成任務1"); // 顯示「任務1完成」的訊息
callback(); // 執行參數提供的回調函數
}, 1000);
}
// 定義第二個任務,接收一個回調函數作為參數
function secondTask(callback) {
// 使用 setTimeout 模擬非同步操作,延遲1秒後執行
setTimeout(() => {
console.log("完成任務2"); // 顯示「任務2」完成的訊息
callback(); // 執行參數提供的回調函數
}, 1000);
}
// 定義第三個任務,接收一個回調函數作為參數
function thirdTask(callback) {
// 使用 setTimeout 模擬非同步操作,延遲1秒後執行
setTimeout(() => {
console.log("完成任務3"); // 顯示「任務3」完成的訊息
callback(); // 執行參數提供的回調函數
}, 1000);
}
// 執行第一個任務,並傳入回調函數
firstTask(() => {
// 第一個任務完成後,執行第二個任務,並傳入回調函數
secondTask(() => {
// 第二個任務完成後,執行第三個任務,並傳入回調函數
thirdTask(() => {
console.log("所有任務完成"); // 打印所有任務完成的訊息
});
});
});
讓我們逐步分析這段代碼的執行順序:
- 第一個任務(firstTask) 開始:
- firstTask 被調用,開始執行其內部的 setTimeout。
- 這個 setTimeout 設定了一個 1 秒的延遲,並預計在 1 秒後執行其回調函數。
- 第二個任務 (secondTask) 準備:
- 1 秒後,firstTask 的 setTimeout 回調函數被執行,打印出「完成任務1」。
- 然後調用 secondTask,它也設置了一個 1 秒的延遲,並預計在 1 秒後執行其回調函數。
- 第三個任務 (thirdTask) 準備:
- 再過 1 秒後,secondTask 的 setTimeout 回調函數被執行,打印出「完成任務2」。
- 然後調用
thirdTask
,它也設置了一個 1 秒的延遲,並預計在 1 秒後執行其回調函數。
- 所有任務完成:
- 又過了 1 秒後,thirdTask 的 setTimeout 回調函數被執行,打印出「完成任務3」。
- 然後執行最內層回調函數,打印出「所有任務完成」。
Promise 如何解決回調地獄
鏈式調用 .then
.t
hen
方法返回一個新的 Promise,這允許我們鏈式調用 .then 方法來處理一系列的非同步操作:
const firstTask = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("完成任務1");
}, 1000);
});
const secondTask = (message) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(message + " -> 完成任務2");
}, 1000);
});
};
const thirdTask = (message) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(message + " -> 完成任務3");
}, 1000);
});
};
firstTask
.then((message) => {
console.log(message); // 打印 "完成任務1"
return secondTask(message);
})
.then((message) => {
console.log(message); // 打印 "完成任務1 -> 完成任務2"
return thirdTask(message);
})
.then((message) => {
console.log(message); // 打印 "完成任務1 -> 完成任務2 -> 完成任務3"
})
.catch((error) => {
console.error(error);
});
這樣寫的好處是:
- 可讀性更高:代碼更直觀,更容易理解各個非同步操作之間的關係。
- 更好的錯誤處理:可以使用
.catch()
來捕獲所有 Promise 中發生的錯誤。 - 避免深度嵌套:通過鏈式調用
.then()
,避免了回調函數的多層嵌套。
使用 Promise
讓我們看看一個稍微複雜一點的示例,假設我們需要從伺服器獲取用戶數據:
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 模擬 API 請求成功或失敗
if (success) {
resolve({ id: userId, name: "John Doe" });
} else {
reject("無法獲取用戶數據");
}
}, 3000);
});
}
fetchUserData(1)
.then((userData) => {
console.log(`用戶ID: ${userData.id}, 用戶名: ${userData.name}`);
})
.catch((error) => {
console.error(error);
});
在這個示例中,我們創建了一個 fetchUserData 函數,它返回一個 Promise。在模擬的非同步操作完成後,如果成功就呼叫 resolve,否則呼叫 reject。
結語
Promise 是處理 JavaScript 非同步操作的強大工具。
它讓你的代碼更具可讀性,也更容易管理錯誤。通過理解 Promise 的基本概念和使用方法,你將能夠更有效地處理非同步操作,提升你的編程技能。
希望這篇文章能幫助你入門 Promise,讓你在學習 JavaScript 的旅程中更上一層樓!