【前端基础】Promise 详解
文章目录
- 什么是 Promise?
- 为什么要使用 Promise?
- 创建 Promise
- 消费 Promise (使用 Promise)
- 1. `.then(onFulfilled, onRejected)`
- 2. `.catch(onRejected)`
- 3. `.finally(onFinally)`
- Promise 链 (Promise Chaining)
- Promise 的静态方法
- 1. `Promise.resolve(value)`
- 2. `Promise.reject(reason)`
- 3. `Promise.all(iterable)`
- 4. `Promise.allSettled(iterable)` (ES2020)
- 5. `Promise.race(iterable)`
- 6. `Promise.any(iterable)` (ES2021)
- Promise 中的错误处理
- Async/Await (基于 Promise 的语法糖)
- 常见陷阱和最佳实践
Promise 是现代 JavaScript 中处理异步操作的核心机制,它极大地改善了回调函数(callback)带来的“回调地狱”问题,使异步代码更加清晰、易于管理和维护。
什么是 Promise?
Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。简单来说,Promise 是一个承诺:它承诺在未来的某个时刻会给你一个结果,这个结果要么是成功的(fulfilled/resolved),要么是失败的(rejected)。
一个 Promise 对象必然处于以下三种状态之一:
- Pending (进行中):初始状态,既没有被兑现,也没有被拒绝。异步操作正在进行中。
- Fulfilled (已兑现/已成功):意味着异步操作成功完成。Promise 有一个与之关联的值(称为 resolved value)。
- Rejected (已拒绝/已失败):意味着异步操作失败。Promise 有一个与之关联的原因(通常是一个 Error 对象,称为 rejection reason)。
一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,它的状态和结果就固定不变了,不能再次改变。这种状态转变是单向的。我们称一个 Promise 为 settled (已敲定),如果它不是 pending 状态(即已兑现或已拒绝)。
为什么要使用 Promise?
-
告别回调地狱 (Callback Hell):
当多个异步操作相互依赖时,使用传统回调函数很容易形成层层嵌套的结构,代码难以阅读和维护。Promise 通过链式调用.then()
来扁平化这种结构。// 回调地狱示例 asyncOperation1(data1, (err1, result1) => {if (err1) { /* ... */ }asyncOperation2(result1, (err2, result2) => {if (err2) { /* ... */ }asyncOperation3(result2, (err3, result3) => {// ...});}); });// 使用 Promise asyncOperation1(data1).then(result1 => asyncOperation2(result1)).then(result2 => asyncOperation3(result2)).then(result3 => { /* ... */ }).catch(error => { /* 处理任何步骤中的错误 */ });
-
更优雅的错误处理:
Promise 提供了.catch()
方法,可以集中处理链中任何一个环节发生的错误,而不需要在每个回调中单独处理。 -
更好的代码可读性和可维护性:
链式调用使得异步操作的流程更加清晰,代码结构更接近同步代码的思维方式。 -
组合异步操作:
Promise 提供了一些静态方法(如Promise.all()
,Promise.race()
)来组合多个 Promise,实现更复杂的异步逻辑。
创建 Promise
可以通过 new Promise(executor)
构造函数来创建一个 Promise 对象。executor
是一个函数,它接收两个参数:resolve
和 reject
。这两个参数本身也是函数。
executor
函数会立即同步执行。- 在
executor
函数内部,通常会执行一些异步操作。 - 当异步操作成功时,调用
resolve(value)
,将 Promise 的状态变为fulfilled
,并将value
作为其结果。 - 当异步操作失败时,调用
reject(reason)
,将 Promise 的状态变为rejected
,并将reason
(通常是一个 Error 对象) 作为其原因。
const myPromise = new Promise((resolve, reject) => {// 模拟一个异步操作console.log("Promise executor 开始执行"); // 这行会立即打印setTimeout(() => {const success = Math.random() > 0.5; // 随机成功或失败if (success) {resolve("操作成功!数据来了!"); // 成功时调用 resolve} else {reject(new Error("操作失败了,呜呜呜...")); // 失败时调用 reject}}, 1000);
});console.log("Promise 已创建"); // 这行也会在 executor 内的 setTimeout 之前打印
消费 Promise (使用 Promise)
创建了 Promise之后,我们需要使用它的结果。这主要通过以下几个方法:
1. .then(onFulfilled, onRejected)
.then()
方法接收两个可选的函数作为参数:
onFulfilled
:当 Promise 状态变为fulfilled
(成功) 时被调用,接收 Promise 的解决值作为参数。onRejected
:当 Promise 状态变为rejected
(失败) 时被调用,接收 Promise 的拒绝原因作为参数。
关键点:.then()
方法返回一个新的 Promise。这使得链式调用成为可能。
myPromise.then((value) => { // onFulfilledconsole.log("成功回调:", value);return "处理成功后的新值"; // 这个返回值会成为下一个 .then 的 onFulfilled 的参数},(error) => { // onRejectedconsole.error("失败回调 (在 then 的第二个参数中):", error.message);// 如果在这里处理了错误,并且没有重新抛出或返回一个 rejected Promise,// 那么链条会继续走向 fulfilled 状态// throw error; // 如果想让链条继续保持 rejected 状态return "从错误中恢复的值";}).then((nextValue) => {console.log("下一个成功回调:", nextValue);},(nextError) => {console.error("下一个失败回调:", nextError.message);});
2. .catch(onRejected)
.catch(onRejected)
方法仅仅是 .then(null, onRejected)
的语法糖。它用于捕获 Promise 链中任何未被处理的拒绝 (rejection)。通常放在 Promise 链的末尾,用于统一处理错误。
myPromise.then(value => {console.log("成功:", value);if (value.includes("数据")) {throw new Error("成功了,但我想抛个错试试!"); // 模拟在 then 中抛错}return value;}).catch(error => { // onRejectedconsole.error("Catch 捕获到错误:", error.message);// 错误在这里被捕获和处理// 如果不重新 throw,链条会认为错误已处理,后续的 .then (如果有 onFulfilled) 可能会执行});
3. .finally(onFinally)
.finally(onFinally)
方法接收一个回调函数 onFinally
,无论 Promise 是 fulfilled
还是 rejected
,这个回调都会被执行。
onFinally
回调不接收任何参数。- 它通常用于执行一些清理工作,比如隐藏加载指示器、关闭数据库连接等。
.finally()
也会返回一个新的 Promise,它会采用原始 Promise 的状态和值/原因 (除非onFinally
抛出错误或返回一个被拒绝的 Promise)。
myPromise.then(value => console.log("Finally 示例 - 成功:", value)).catch(error => console.error("Finally 示例 - 失败:", error.message)).finally(() => {console.log("Finally: 操作已完成,无论成功或失败都会执行我!");// 通常不在这里改变 Promise 的最终结果});
Promise 链 (Promise Chaining)
由于 .then()
, .catch()
, 和 .finally()
都返回新的 Promise 对象,我们可以将它们串联起来,形成一个 Promise 链。
- 传递值:前一个
.then
的onFulfilled
回调的返回值会作为下一个.then
的onFulfilled
回调的参数。 - 异步操作的串行化:如果
.then
的回调函数返回一个新的 Promise,那么链条会等待这个新的 Promise 完成后,才会继续执行后续的.then
。
function step1() {return new Promise(resolve => {setTimeout(() => {console.log("步骤1完成");resolve("来自步骤1的结果");}, 500);});
}function step2(dataFromStep1) {return new Promise(resolve => {setTimeout(() => {console.log("步骤2完成,接收到:", dataFromStep1);resolve("来自步骤2的结果");}, 500);});
}function step3(dataFromStep2) {return new Promise(resolve => {setTimeout(() => {console.log("步骤3完成,接收到:", dataFromStep2);resolve("最终结果");}, 500);});
}step1().then(result1 => step2(result1)) // step2 返回一个 Promise.then(result2 => step3(result2)) // step3 返回一个 Promise.then(finalResult => {console.log("所有步骤完成:", finalResult);}).catch(error => {console.error("链条中发生错误:", error);});
Promise 的静态方法
Promise
对象本身也提供了一些有用的静态方法:
1. Promise.resolve(value)
返回一个使用给定值解决的 Promise 对象。
- 如果
value
本身是一个 Promise,则返回这个 Promise。 - 如果
value
是一个 thenable 对象(即拥有.then
方法的对象),Promise.resolve
会尝试展开这个 thenable 对象,并采用其最终状态。 - 否则,返回的 Promise 将会以
value
值完成。
Promise.resolve("立即成功").then(v => console.log(v)); // 输出: 立即成功const p = new Promise(r => setTimeout(() => r("延迟成功"), 100));
Promise.resolve(p).then(v => console.log(v)); // 等待 p 完成后输出: 延迟成功
2. Promise.reject(reason)
返回一个以给定原因拒绝的 Promise 对象。
Promise.reject(new Error("立即失败")).catch(e => console.error(e.message)); // 输出: 立即失败
3. Promise.all(iterable)
接收一个 Promise 对象的可迭代对象 (例如数组) 作为参数。
- 返回一个新的 Promise。
- 这个新的 Promise 只有在可迭代对象中所有的 Promise 都成功 (fulfilled) 时才会成功。其成功的值是一个数组,包含了所有输入 Promise 的成功值,顺序与输入 Promise 在可迭代对象中的顺序一致。
- 如果可迭代对象中的任何一个 Promise 失败 (rejected),那么
Promise.all()
返回的 Promise 会立即失败,并以第一个失败的 Promise 的原因为准。
const promise1 = Promise.resolve(3);
const promise2 = 42; // 会被 Promise.resolve(42) 包装
const promise3 = new Promise((resolve, reject) => {setTimeout(resolve, 100, 'foo');
});Promise.all([promise1, promise2, promise3]).then((values) => {console.log("Promise.all 成功:", values); // 输出: [3, 42, "foo"]
}).catch(error => {console.error("Promise.all 失败:", error);
});const promiseWithError = Promise.reject('错误了');
Promise.all([promise1, promiseWithError, promise3]).then((values) => {// 这段不会执行
}).catch(error => {console.error("Promise.all 因为一个错误而失败:", error); // 输出: 错误了
});
4. Promise.allSettled(iterable)
(ES2020)
接收一个 Promise 对象的可迭代对象作为参数。
- 返回一个新的 Promise。
- 这个新的 Promise 会在可迭代对象中所有的 Promise 都已敲定 (settled) 后(无论是 fulfilled 还是 rejected)才会完成。
- 其成功的值是一个对象数组,每个对象描述了对应 Promise 的结果,格式为:
- 成功:
{ status: 'fulfilled', value: resolvedValue }
- 失败:
{ status: 'rejected', reason: rejectionReason }
- 成功:
这对于你希望等待多个异步操作完成,但不在乎它们是否都成功的情况非常有用。
const pSuccess = Promise.resolve("成功的值");
const pFailure = Promise.reject("失败的原因");
const pPending = new Promise(r => setTimeout(() => r("稍后成功"), 200));Promise.allSettled([pSuccess, pFailure, pPending]).then(results => {results.forEach(result => {if (result.status === 'fulfilled') {console.log(`AllSettled - Fulfilled: ${result.value}`);} else {console.error(`AllSettled - Rejected: ${result.reason}`);}});// 输出:// AllSettled - Fulfilled: 成功的值// AllSettled - Rejected: 失败的原因// (等待200ms后) AllSettled - Fulfilled: 稍后成功
});
5. Promise.race(iterable)
接收一个 Promise 对象的可迭代对象作为参数。
- 返回一个新的 Promise。
- 这个新的 Promise 会在可迭代对象中的任何一个 Promise 最先敲定 (settled) 时立即敲定,并采用第一个敲定的 Promise 的状态和结果(无论是 fulfilled 还是 rejected)。
const pFast = new Promise((resolve) => setTimeout(() => resolve('快速'), 50));
const pSlow = new Promise((resolve) => setTimeout(() => resolve('慢速'), 200));
const pReject = new Promise((resolve, reject) => setTimeout(() => reject('立即拒绝'), 10));Promise.race([pFast, pSlow]).then(value => console.log("Race 胜出 (fast/slow):", value)) // 输出: 快速.catch(error => console.error("Race 失败 (fast/slow):", error));Promise.race([pFast, pReject]).then(value => console.log("Race 胜出 (fast/reject):", value)).catch(error => console.error("Race 失败 (fast/reject):", error)); // 输出: 立即拒绝
6. Promise.any(iterable)
(ES2021)
接收一个 Promise 对象的可迭代对象作为参数。
- 返回一个新的 Promise。
- 这个新的 Promise 会在可迭代对象中的任何一个 Promise 最先成功 (fulfilled) 时立即成功,并以第一个成功的 Promise 的值为准。
- 如果可迭代对象中的所有 Promise 都失败 (rejected),那么
Promise.any()
返回的 Promise 会失败,并以一个AggregateError
对象为原因。AggregateError
对象有一个errors
属性,它是一个包含了所有拒绝原因的数组。
const pErr1 = Promise.reject('第一个错误');
const pErr2 = Promise.reject('第二个错误');
const pSucc1 = new Promise(r => setTimeout(() => r('第一个成功'), 50));
const pSucc2 = new Promise(r => setTimeout(() => r('第二个成功'), 10));Promise.any([pErr1, pSucc1, pSucc2]).then(value => console.log("Any 成功:", value)) // 输出: 第二个成功 (因为它最先 fulfilled).catch(error => console.error("Any 失败:", error));Promise.any([pErr1, pErr2]).then(value => console.log("Any 成功 (全失败场景):", value)) // 不会执行.catch(error => {console.error("Any 失败 (全失败场景):", error.name); // AggregateErrorconsole.error(error.errors); // ['第一个错误', '第二个错误']});
Promise 中的错误处理
正确处理 Promise 中的错误至关重要。
-
使用
.catch()
:
最佳实践是在 Promise 链的末尾添加一个.catch()
来捕获所有未处理的拒绝。 -
.then()
的第二个参数:
虽然.then(onFulfilled, onRejected)
可以处理错误,但它不如.catch()
清晰,且如果onFulfilled
抛出错误,该错误不会被同一个.then
的onRejected
捕获,而是会传递给链中的下一个.catch()
或onRejected
。 -
Executor 中的同步错误:
如果在new Promise(executor)
的executor
函数中抛出同步错误,这个错误会被隐式捕获,并导致 Promise 立即变为 rejected 状态。new Promise((resolve, reject) => {throw new Error("Executor 中的同步错误"); }).catch(err => console.error(err.message)); // 输出: Executor 中的同步错误
-
未捕获的 Promise 拒绝 (Unhandled Rejection):
如果一个 Promise 被拒绝,并且链上没有任何.catch()
或onRejected
处理它,浏览器通常会在控制台报告一个 “Uncaught (in promise)” 错误。Node.js 环境中,这可能会导致应用进程终止(取决于版本和配置)。所以,务必处理所有可能的拒绝。
Async/Await (基于 Promise 的语法糖)
ES2017 引入了 async
和 await
关键字,它们是构建在 Promise 之上的语法糖,使得异步代码的写法更像同步代码,从而更加直观。
async function
:声明一个异步函数。异步函数会自动返回一个 Promise。如果函数内部返回一个非 Promise 值,这个值会被Promise.resolve()
包装。如果函数内部抛出错误,返回的 Promise 会被 reject。await
:只能在async function
内部使用。它会暂停异步函数的执行,等待await
后面的 Promise 完成(resolved 或 rejected)。- 如果 Promise resolved,
await
表达式的值就是 Promise 的解决值。 - 如果 Promise rejected,
await
会抛出这个拒绝的原因(通常是一个 Error 对象),这可以被async function
内部的try...catch
语句捕获。
- 如果 Promise resolved,
function fetchData(shouldFail = false) {return new Promise((resolve, reject) => {setTimeout(() => {if (shouldFail) {reject(new Error("数据获取失败 (async/await)"));} else {resolve("数据已获取 (async/await)");}}, 1000);});
}async function processData() {console.log("开始处理数据...");try {const data1 = await fetchData(); // 等待 fetchData() 完成console.log("第一份数据:", data1);const data2 = await fetchData(true); // 尝试获取会失败的数据console.log("第二份数据:", data2); // 这行不会执行} catch (error) {console.error("Async/await 捕获到错误:", error.message);} finally {console.log("数据处理流程结束 (async/await)");}
}processData();
// 输出顺序:
// 开始处理数据...
// (等待1秒)
// 第一份数据: 数据已获取 (async/await)
// (等待1秒)
// Async/await 捕获到错误: 数据获取失败 (async/await)
// 数据处理流程结束 (async/await)
async/await
极大地简化了异步流程控制,尤其是当涉及到多个 последовательных (sequential) 异步操作时。
常见陷阱和最佳实践
-
忘记
return
:在.then()
回调中,如果进行了异步操作并创建了新的 Promise,或者想将值传递给下一个.then()
,确保return
这个 Promise 或值。否则,链条可能会意外地提前继续,或者下一个.then()
接收到undefined
。 -
“吞没”错误:在
.catch()
或onRejected
中,如果没有显式地重新抛出错误 (throw error;
) 或返回一个被拒绝的 Promise,那么 Promise 链会认为错误已经被处理,并可能转为fulfilled
状态。 -
不必要的 Promise 包装:避免所谓的 “Promise constructor anti-pattern”,即在
new Promise
的 executor 中包装已经返回 Promise 的函数。// 反模式 function getUserDataWrapper(userId) {return new Promise((resolve, reject) => { // 不必要的 Promise 包装fetch(`/api/users/${userId}`) // fetch 本身返回 Promise.then(response => response.json()).then(data => resolve(data)).catch(error => reject(error));}); }// 更好的方式 function getUserData(userId) {return fetch(`/api/users/${userId}`).then(response => {if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return response.json();}); }
-
总是添加
.catch()
:对于任何 Promise 链,都应该在末尾添加一个.catch()
来处理潜在的未捕获错误。 -
Promise.all
vsPromise.allSettled
:根据你的需求选择。如果任何一个失败都应该导致整个操作失败,使用Promise.all
。如果希望所有操作都尝试完成,并分别处理它们的结果,使用Promise.allSettled
。 -
理解微任务队列:Promise 的
.then
,.catch
,.finally
的回调会作为微任务 (microtask) 被放入微任务队列中,它们会在当前宏任务 (macrotask) 执行完毕后、下一个宏任务开始前立即执行。这对于理解 Promise 的执行时机非常重要。
Promise 是 JavaScript 异步编程的基石,深刻理解它对于编写健壮、可维护的现代 JavaScript 应用至关重要。