每日前端手写题Day12:手写Promise.all与Promise.race实现解析
2025.09.19 12:47浏览量:0简介:本文深入解析Promise.all与Promise.race的核心机制,通过手写实现帮助开发者掌握异步编程关键技巧,提升代码质量与调试能力。
一、为何需要手写Promise工具方法?
Promise作为ES6引入的异步处理方案,极大简化了回调地狱问题。但实际开发中,我们常依赖Promise.all
和Promise.race
等工具方法处理并发请求。手写这些方法不仅能加深对Promise工作原理的理解,还能在特殊场景下(如需要定制错误处理逻辑)实现更灵活的控制。
典型应用场景
- 批量请求处理:同时发起多个API请求,等待全部完成或任一完成
- 超时控制:设置请求超时机制,避免长时间等待
- 优先级请求:按优先级顺序处理多个异步任务
二、Promise.all实现详解
核心机制
Promise.all
接收一个Promise对象数组,返回一个新的Promise:
- 当所有输入Promise都成功时,返回包含各结果的数组(顺序与输入一致)
- 当任一Promise失败时,立即返回第一个失败的Promise结果
手写实现代码
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
// 处理空数组情况
if (promises.length === 0) {
resolve([]);
return;
}
const results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
// 确保每个元素都是Promise
Promise.resolve(promise)
.then(value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
})
.catch(error => {
reject(error);
});
});
});
}
关键点解析
- 参数验证:使用
Promise.resolve()
确保非Promise值也能被处理 - 顺序保持:通过索引
index
保持结果数组顺序与输入一致 - 完成判断:使用计数器
completedCount
精确判断所有Promise完成时机 - 错误处理:任一Promise失败立即触发reject
测试用例
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const p3 = Promise.reject('error');
myPromiseAll([p1, p2, p3])
.then(console.log) // 不会执行
.catch(console.error); // 输出: "error"
三、Promise.race实现详解
核心机制
Promise.race
接收一个Promise对象数组,返回一个新的Promise:
- 当任一输入Promise解决或拒绝时,立即返回该结果
- 后续Promise的结果将被忽略
手写实现代码
function myPromiseRace(promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
// 空数组处理:根据ES规范,应该永远挂起
// 这里简化处理为立即resolve,实际应根据需求调整
resolve(undefined);
return;
}
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve)
.catch(reject);
});
});
}
关键点解析
- 竞态条件处理:第一个完成(无论成功失败)的Promise决定结果
- 资源释放:后续Promise的结果被忽略,但仍在执行(需注意资源泄漏)
- 空数组处理:规范未明确,实际开发中应根据业务需求决定
实际应用示例:请求超时控制
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
);
return myPromiseRace([fetchPromise, timeoutPromise]);
}
// 使用示例
fetchWithTimeout('https://api.example.com/data')
.then(response => console.log('Success:', response))
.catch(error => console.error('Failed:', error));
四、进阶实现与优化
带进度的Promise.all
function myPromiseAllWithProgress(promises, onProgress) {
return new Promise((resolve, reject) => {
const results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
results[index] = value;
completedCount++;
onProgress && onProgress(completedCount / promises.length);
if (completedCount === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
}
错误处理增强版
function myPromiseAllSafe(promises, defaultValues = []) {
return new Promise((resolve) => {
const results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
})
.catch(() => {
results[index] = defaultValues[index] || null;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
});
});
});
}
五、最佳实践建议
- 输入验证:在实际项目中应添加参数类型检查
- 性能优化:对于大量Promise,考虑分批处理
- 错误处理:根据业务需求决定是立即失败还是收集所有错误
- 取消机制:可结合AbortController实现请求取消
- TypeScript支持:添加类型定义提升代码可靠性
六、常见问题解答
Q1: 为什么我的实现中结果顺序不对?
A1: 必须使用输入时的索引来存储结果,不能简单push到数组
Q2: 如何处理非Promise输入?
A2: 使用Promise.resolve()
包装每个输入项,确保统一处理
Q3: 空数组应该返回什么?
A3: 根据ES规范,Promise.all([])
应返回已解决的Promise(值为空数组),Promise.race([])
应永远挂起
通过系统掌握这些核心方法的实现原理,开发者不仅能更灵活地处理异步场景,还能在面试和实际项目中展现出更深的技术功底。建议结合实际项目需求,对这些基础实现进行扩展和优化。
发表评论
登录后可评论,请前往 登录 或 注册