一、为什么需要 Promise?------ 解决"回调地狱"
在 Promise 出现之前,我们依赖回调函数来处理异步操作。当多个异步操作需要顺序执行时,代码就会变成这样:
javascript
doFirstThing(function(result1) {
doSecondThing(result1, function(result2) {
doThirdThing(result2, function(result3) {
console.log('最终结果: ' + result3);
}, failureCallback);
}, failureCallback);
}, failureCallback);
这种"金字塔"形的代码被称为 "回调地狱" 或 "厄运金字塔"。它有很多问题:
- 难以阅读和维护:代码嵌套层次深,逻辑混乱。
- 错误处理困难:必须在每一层单独处理错误,非常繁琐。
- 缺乏可组合性:难以对多个异步操作进行组合(如"等到所有操作完成"或"等到第一个操作完成")。
Promise 的诞生就是为了优雅地解决这些问题。
二、Promise 是什么?
一个 Promise 是一个对象,它代表了一个异步操作的最终完成(或失败)及其结果值。
可以把它想象成一个承诺:它承诺未来会给你一个结果(可能是成功的数据,也可能是失败的原因),而这个承诺有三种状态:
- pending(待定): 初始状态,既没有被兑现,也没有被拒绝。
- fulfilled(已兑现) : 意味着操作成功完成。此时会有一个不可变的结果值。
- rejected(已拒绝) : 意味着操作失败。此时会有一个不可变的拒绝原因。
Promise 的状态一旦改变(从 pending
变为 fulfilled
或 rejected
),就永久定型了,不会再变。这也是其英文名"承诺"的由来。
三、基本语法
javascript
const myPromise = new Promise((resolve, reject) => {
// 这是一个执行器函数 (Executor),会立即同步执行
// 模拟一个异步操作(比如网络请求)
setTimeout(() => {
const success = true; // 模拟操作成功或失败
if (success) {
resolve('操作成功!这是得到的数据。'); // 将状态变为 fulfilled,并传递结果
} else {
reject(new Error('操作失败!')); // 将状态变为 rejected,并传递原因
}
}, 1000);
});
创建好 Promise 后,我们使用 .then()
, .catch()
, 和 .finally()
方法来处理结果。
四、消费 Promise:.then()、.catch()、.finally()
1. .then()
.then()
方法接收两个参数(都是可选的):
- 第一个参数是 fulfilled 状态的回调函数。
- 第二个参数是 rejected 状态的回调函数(不常用,通常用
.catch()
代替)。
javascript
myPromise.then(
(result) => {
console.log(result); // "操作成功!这是得到的数据。"
},
(error) => {
console.error(error); // 这里不会执行,因为上面成功了
}
);
2. .catch()
.catch()
是 .then(null, rejectionCallback)
的语法糖,专门用于处理错误。
javascript
myPromise
.then((result) => {
console.log(result);
})
.catch((error) => { // 捕获链中任何阶段的错误
console.error('糟了!', error);
});
3. .finally()
.finally()
方法无论 Promise 最终状态如何都会执行。它非常适合做清理工作,比如关闭加载动画。
javascript
myPromise
.then((result) => { /* ... */ })
.catch((error) => { /* ... */ })
.finally(() => {
console.log('无论成功失败,我都会执行');
});
五、Promise 链(Chaining)------ 核心优势
这是 Promise 最强大的特性。因为 .then()
方法总是返回一个新的 Promise,所以你可以将它们链式调用,从而扁平化地解决回调地狱。
javascript
doFirstThing() // 假设返回一个 Promise
.then((result1) => {
console.log('第一步成功:', result1);
return doSecondThing(result1); // 返回一个新的 Promise
})
.then((result2) => { // 等待 doSecondThing 的结果
console.log('第二步成功:', result2);
return doThirdThing(result2);
})
.then((result3) => {
console.log('最终结果:', result3);
})
.catch((error) => { // 一个 .catch 就能捕获前面所有步骤的错误
console.error('某一步出错了:', error);
});
关键点:
- 在
.then()
的回调中,如果你返回一个值 ,它会被包装成一个 fulfilled 状态的 Promise,成为下一个.then()
的参数。 - 如果你返回一个 Promise ,下一个
.then()
会等待这个 Promise 解决后再执行。 - 如果你抛出错误 (
throw new Error(...)
),它会创建一个 rejected 状态的 Promise,直接被链末端的.catch()
捕获。
六、常用的静态方法
Promise 构造函数本身也提供了一些有用的类方法。
1. Promise.all([promise1, promise2, ...])
等待所有输入的 Promise 都成功 ,返回一个包含所有结果值的数组。 如果有一个被拒绝,则立即拒绝,并返回第一个被拒绝的原因。
javascript
const promise1 = fetch('/api/1');
const promise2 = fetch('/api/2');
Promise.all([promise1, promise2])
.then(([result1, result2]) => { // 结果顺序与输入顺序一致
console.log('两个请求都成功了', result1, result2);
})
.catch((error) => {
console.log('至少有一个请求失败了', error);
});
2. Promise.race([promise1, promise2, ...])
竞速。返回第一个敲定(无论是 fulfilled 还是 rejected)的 Promise 的结果或原因。
javascript
// 常用于设置请求超时
const fetchPromise = fetch('/api/data');
const timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('请求超时')), 5000);
});
Promise.race([fetchPromise, timeoutPromise])
.then((result) => {
// 在超时前正常返回
})
.catch((error) => {
// 要么是网络请求错误,要么是超时错误
});
3. Promise.allSettled([promise1, promise2, ...])
ES2020 引入。等待所有 Promise 都被敲定(每个都完成或拒绝)。返回一个数组,每个元素是一个对象,描述了每个 Promise 的结果。
javascript
Promise.allSettled([promise1, promise2]).then((results) => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});
4. Promise.any([promise1, promise2, ...])
ES2021 引入 。返回第一个 fulfilled 的 Promise。只有当所有输入的 Promise 都被拒绝时,它才会拒绝。
javascript
// 从多个镜像源请求同一个资源,只要有一个成功就行
Promise.any([fetch('mirror1.com'), fetch('mirror2.com'), fetch('mirror3.com')])
.then((firstSuccess) => {
console.log('第一个成功的响应:', firstSuccess);
})
.catch((error) => {
console.log('所有镜像源都失败了', error);
});
七、总结与建议
优点:
- 链式调用:将异步操作以同步代码的流程表达出来,避免了回调地狱。
- 统一的错误处理 :通过一个
.catch()
可以捕获整个链路上的错误。 - 提供强大的组合器 :
Promise.all
,Promise.race
等让并发控制变得非常简单。
注意点:
- Promise 一旦创建,无法被取消。
- 如果不设置
.catch()
回调,Promise 内部的错误会被 silently swallowed(静默吞掉),在浏览器中你可能会在控制台看到一个未捕获的错误警告。
现代异步编程 : 虽然 Promise 很好,但 ES2017 的 async/await
语法让它更上一层楼。async/await
是基于 Promise 的语法糖,让你能用完全同步的代码风格来编写异步逻辑,可读性极高。
javascript
// 使用 async/await 重写上面的 Promise 链
async function handleOperations() {
try {
const result1 = await doFirstThing();
console.log('第一步成功:', result1);
const result2 = await doSecondThing(result1);
console.log('第二步成功:', result2);
const result3 = await doThirdThing(result2);
console.log('最终结果:', result3);
} catch (error) {
console.error('某一步出错了:', error);
}
}
handleOperations();