今天,我想和大家深入聊聊 Promise
的几个高阶方法:Promise.all
、Promise.race
、Promise.allSettled
和 Promise.any
。这些方法在处理多个异步任务时非常有用,也是面试中的常客。我会从基本用法讲起,结合实际例子,最后手写实现这些方法,并附上常见的面试题。
1. Promise.all:等待所有任务完成
1.1 基本概念
Promise.all
是一个静态方法,它接收一个可迭代对象(通常是数组),其中包含多个 Promise
实例。它的作用是等待所有 Promise
都成功完成 ,然后返回一个新 Promise
,该 Promise
的结果是一个包含所有 Promise
结果的数组。
关键点:
- 所有
Promise
都必须成功,Promise.all
返回的Promise
才会成功。 - 如果其中任何一个
Promise
失败(rejected),Promise.all
会立即失败,并返回第一个失败的Promise
的错误。
1.2 使用场景
想象一下,你正在开发一个电商网站,需要同时获取用户信息、购物车数据和推荐商品列表。这三个请求是独立的,但你希望在所有数据都准备好后,再渲染页面。
javascript
// 模拟三个异步请求
function fetchUserInfo() {
return new Promise((resolve) => {
setTimeout(() => resolve({ name: '张三', age: 25 }), 1000);
});
}
function fetchCartData() {
return new Promise((resolve) => {
setTimeout(() => resolve(['商品A', '商品B']), 800);
});
}
function fetchRecommendations() {
return new Promise((resolve) => {
setTimeout(() => resolve(['商品C', '商品D']), 1200);
});
}
// 使用 Promise.all
Promise.all([fetchUserInfo(), fetchCartData(), fetchRecommendations()])
.then(([userInfo, cartData, recommendations]) => {
console.log('用户信息:', userInfo);
console.log('购物车:', cartData);
console.log('推荐商品:', recommendations);
// 渲染页面
})
.catch(error => {
console.error('获取数据失败:', error);
// 处理错误,比如显示错误提示
});
在这个例子中,Promise.all
等待所有三个请求都成功后,才执行 .then
回调。如果任何一个请求失败(比如网络错误),Promise.all
会立即进入 .catch
分支。
1.3 手写实现
现在我们来手写一个 Promise.all
的简化版本:
javascript
function myPromiseAll(promises) {
// 返回一个新的 Promise
return new Promise((resolve, reject) => {
// 如果传入的不是可迭代对象,直接 resolve 空数组
if (!Array.isArray(promises)) {
return resolve([]);
}
const results = [];
let completedCount = 0;
// 如果传入空数组,直接 resolve
if (promises.length === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
// 使用 Promise.resolve 确保每个元素都是 Promise
Promise.resolve(promise)
.then(value => {
results[index] = value;
completedCount++;
// 当所有 Promise 都完成时,resolve 结果数组
if (completedCount === promises.length) {
resolve(results);
}
})
.catch(error => {
// 任何一个 Promise 失败,立即 reject
reject(error);
});
});
});
}
// 测试
myPromiseAll([
Promise.resolve('第一个'),
new Promise(resolve => setTimeout(() => resolve('第二个'), 500)),
'第三个' // 普通值也会被包装成 Promise
]).then(results => {
console.log(results); // ['第一个', '第二个', '第三个']
});
1.4 注意事项
Promise.all
的结果数组顺序与传入的Promise
数组顺序一致,不保证执行顺序。- 如果传入的
Promise
中有reject
,Promise.all
会立即失败,不会等待其他Promise
完成。
2. Promise.race:谁先完成就用谁
2.1 基本概念
Promise.race
也是接收一个 Promise
数组,但它只关心第一个完成的 Promise
,无论是成功还是失败。一旦有 Promise
完成,Promise.race
就会立即返回那个结果。
关键点:
- 返回第一个完成的
Promise
的结果或错误。 - 适用于超时控制或竞态场景。
2.2 使用场景
最常见的用途是实现请求超时。比如,我们希望一个请求在 3 秒内完成,否则就认为它失败了。
javascript
// 模拟一个可能很慢的请求
function slowRequest() {
return new Promise(resolve => {
setTimeout(() => resolve('请求成功'), 5000); // 5秒
});
}
// 创建一个超时的 Promise
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`请求超时: ${ms}ms`)), ms);
});
}
// 使用 Promise.race 实现超时
Promise.race([slowRequest(), timeout(3000)])
.then(result => {
console.log('成功:', result);
})
.catch(error => {
console.error('失败:', error.message); // 输出: 失败: 请求超时: 3000ms
});
在这个例子中,timeout(3000)
会在 3 秒后 reject
,而 slowRequest()
需要 5 秒。由于 timeout
先完成,Promise.race
会立即进入 .catch
分支。
2.3 手写实现
javascript
function myPromiseRace(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises) || promises.length === 0) {
return reject(new Error('Promise.race requires a non-empty iterable'));
}
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve) // 第一个 resolve 或 reject 会决定最终结果
.catch(reject);
});
});
}
// 测试
myPromiseRace([
new Promise(resolve => setTimeout(() => resolve('A'), 100)),
new Promise(resolve => setTimeout(() => resolve('B'), 50)),
new Promise((_, reject) => setTimeout(() => reject(new Error('C')), 30))
])
.then(result => {
console.log(result); // 不会执行,因为 C 先 reject
})
.catch(error => {
console.error(error.message); // 输出: C
});
2.4 注意事项
Promise.race
的"完成"包括resolve
和reject
。只要有一个Promise
结束,它就结束。- 适合用于竞态场景,比如多个数据源中取最快的一个。
3. Promise.allSettled:等待所有任务尘埃落定
3.1 基本概念
Promise.allSettled
是 ES2020 引入的新方法。它和 Promise.all
类似,都会等待所有 Promise
完成,但关键区别在于:它不会因为某个 Promise
失败而中断 。无论成功还是失败,它都会等待所有 Promise
结束,并返回一个描述每个 Promise
结果的对象数组。
返回值结构:
- 成功的
Promise
:{ status: 'fulfilled', value: result }
- 失败的
Promise
:{ status: 'rejected', reason: error }
3.2 使用场景
当你需要知道所有 异步任务的结果,而不关心它们是否成功时,Promise.allSettled
就派上用场了。比如批量上传文件,你希望知道每个文件的上传结果,而不是因为一个失败就放弃。
javascript
function uploadFile(filename) {
return new Promise((resolve, reject) => {
const success = Math.random() > 0.3; // 模拟 70% 成功率
setTimeout(() => {
success ? resolve(`${filename} 上传成功`) : reject(new Error(`${filename} 上传失败`));
}, 1000);
});
}
const files = ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt'];
Promise.allSettled(files.map(uploadFile))
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${files[index]}: ✅ ${result.value}`);
} else {
console.log(`${files[index]}: ❌ ${result.reason.message}`);
}
});
});
输出可能类似于:
makefile
file1.txt: ✅ file1.txt 上传成功
file2.txt: ❌ file2.txt 上传失败
file3.txt: ✅ file3.txt 上传成功
file4.txt: ✅ file4.txt 上传成功
3.3 手写实现
javascript
function myPromiseAllSettled(promises) {
return new Promise((resolve) => {
if (!Array.isArray(promises)) {
return resolve([]);
}
const results = [];
let settledCount = 0;
if (promises.length === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
results[index] = { status: 'fulfilled', value };
settledCount++;
if (settledCount === promises.length) {
resolve(results);
}
})
.catch(reason => {
results[index] = { status: 'rejected', reason };
settledCount++;
if (settledCount === promises.length) {
resolve(results);
}
});
});
});
}
// 测试
myPromiseAllSettled([
Promise.resolve('成功1'),
Promise.reject(new Error('失败1')),
'普通值'
]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: '成功1' },
// { status: 'rejected', reason: Error: 失败1 },
// { status: 'fulfilled', value: '普通值' }
// ]
});
3.4 注意事项
Promise.allSettled
永远不会 reject,它总是 resolve 一个结果数组。- 适合用于批量操作,需要收集所有结果的场景。
4. Promise.any:至少有一个成功
4.1 基本概念
Promise.any
是 ES2021 引入的方法。它会等待第一个成功(fulfilled)的 Promise
,并返回其结果。只有当所有 Promise
都失败时,它才会 reject,并返回一个 AggregateError
,其中包含所有失败的原因。
关键点:
- 返回第一个成功的
Promise
的结果。 - 只有当所有
Promise
都失败时,才会 reject。
4.2 使用场景
想象你需要从多个镜像源下载一个文件,只要有一个源成功,就可以下载。这比 Promise.race
更合适,因为 race
会返回第一个结果(可能是失败),而 any
只关心成功。
javascript
function downloadFromSource(source) {
return new Promise((resolve, reject) => {
const success = Math.random() > 0.6; // 模拟 40% 成功率
setTimeout(() => {
success ? resolve(`从 ${source} 下载成功`) : reject(new Error(`从 ${source} 下载失败`));
}, 1000 + Math.random() * 1000);
});
}
const sources = ['源A', '源B', '源C', '源D'];
Promise.any(sources.map(downloadFromSource))
.then(result => {
console.log('下载成功:', result);
})
.catch(error => {
console.error('所有源都失败:', error.errors); // AggregateError
});
如果至少有一个源成功,就会进入 .then
;如果全部失败,才会进入 .catch
。
4.3 手写实现
javascript
function myPromiseAny(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises) || promises.length === 0) {
return reject(new AggregateError([], 'All promises were rejected'));
}
const errors = [];
let rejectedCount = 0;
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve) // 第一个成功就 resolve
.catch(error => {
errors.push(error);
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
}
// 测试
myPromiseAny([
Promise.reject(new Error('失败1')),
new Promise(resolve => setTimeout(() => resolve('成功2'), 500)),
Promise.reject(new Error('失败3'))
])
.then(result => {
console.log(result); // 成功2
})
.catch(error => {
console.error('全部失败:', error.errors);
});
4.4 注意事项
Promise.any
会忽略失败,直到找到第一个成功。- 如果所有
Promise
都失败,它会 reject 一个AggregateError
。
5. 四种方法对比总结
方法 | 成功条件 | 失败条件 | 返回值 | 适用场景 |
---|---|---|---|---|
Promise.all |
所有成功 | 任一失败 | 成功值数组 | 所有任务必须成功 |
Promise.race |
第一个完成(无论成功/失败) | 第一个完成(无论成功/失败) | 第一个结果 | 竞态、超时 |
Promise.allSettled |
所有完成(无论成功/失败) | 永不失败 | 结果对象数组 | 收集所有结果 |
Promise.any |
第一个成功 | 所有失败 | 第一个成功值 | 至少一个成功 |
6. 常见面试题
-
Promise.all
和Promise.race
的区别是什么?all
等待所有成功,race
等待第一个完成。
-
如果
Promise.all
中有一个Promise
失败,会发生什么?Promise.all
会立即 reject,并返回第一个失败的Promise
的错误。
-
Promise.allSettled
和Promise.all
有什么不同?allSettled
不会因为某个Promise
失败而中断,它会等待所有Promise
完成并返回结果。
-
Promise.any
在什么情况下会 reject?- 只有当所有
Promise
都 reject 时,Promise.any
才会 reject。
- 只有当所有
-
手写
Promise.all
的实现。- 如上文所示,注意处理空数组、非数组输入和
Promise.resolve
包装。
- 如上文所示,注意处理空数组、非数组输入和
-
如何实现一个带超时功能的
Promise
?- 使用
Promise.race
,结合一个定时 reject 的Promise
。
- 使用
-
Promise.all
的结果数组顺序是否与传入数组一致?- 是的,顺序与传入的
Promise
数组顺序一致。
- 是的,顺序与传入的
-
为什么
Promise.all
要用Promise.resolve
包装每个元素?- 为了确保每个元素都是
Promise
,兼容普通值或 thenable 对象。
- 为了确保每个元素都是
结语
Promise
的这些高阶方法极大地丰富了我们处理异步操作的能力。理解它们的差异和适用场景,不仅能写出更健壮的代码,也能在面试中游刃有余。记住,选择哪个方法取决于你的业务需求:
- 要全部成功 ?用
all
。 - 要第一个结果 ?用
race
。 - 要所有结果 ?用
allSettled
。 - 要至少一个成功 ?用
any
。
希望这篇博客能帮你彻底掌握这些 Promise
的高阶用法。在实际开发中多尝试使用它们,你会发现异步编程可以如此优雅。