1. 为什么需要封装Promise.all
在JavaScript异步编程中,Promise.all是一个极其重要的工具方法。它允许我们并行执行多个Promise,并等待所有Promise都完成后再统一处理结果。但在实际开发中,原生的Promise.all存在几个明显的痛点:
首先,原生Promise.all只要有一个Promise被reject,整个Promise.all就会立即reject。这在很多业务场景下并不合理,比如我们需要同时请求多个接口,即使某些接口失败了,仍然希望获取其他成功接口的数据。
其次,原生Promise.all缺乏对每个Promise执行状态的细粒度控制。我们无法知道每个Promise是成功还是失败,也无法对失败的Promise进行特殊处理。
最后,原生Promise.all没有提供超时机制。如果某个Promise长时间不返回,整个Promise.all就会一直等待,这在生产环境中是非常危险的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Promise.all的封装设计思路
2.1 核心功能需求分析
基于上述痛点,我们需要设计一个增强版的Promise.all封装,它应该具备以下核心功能:
- 即使部分Promise失败,也能返回所有Promise的结果(包括成功和失败的)
- 能够区分每个Promise的执行状态
- 支持全局超时和单个Promise超时设置
- 提供更友好的错误处理机制
- 保持与原生Promise.all相似的API设计,降低使用成本
2.2 技术实现方案
我们将创建一个名为promiseAllSettled的函数,它接收一个Promise数组和可选的配置对象作为参数。核心实现思路如下:
- 对每个Promise进行包装,捕获可能的reject
- 记录每个Promise的执行状态和结果
- 实现超时控制机制
- 返回统一格式的结果数组
3. 完整实现代码解析
3.1 基础版本实现
让我们先实现一个基础版本,解决最核心的"不因单个失败而整体失败"的问题:
javascript复制function promiseAllSettled(promises) {
// 将每个Promise包装,确保不会被reject中断
const wrappedPromises = promises.map(p =>
Promise.resolve(p)
.then(value => ({ status: 'fulfilled', value }))
.catch(reason => ({ status: 'rejected', reason }))
);
// 使用原生Promise.all执行包装后的Promise
return Promise.all(wrappedPromises);
}
这个基础版本已经解决了最核心的问题,但它还缺乏很多实用的功能。让我们继续完善它。
3.2 增强版实现
下面是一个功能更完整的实现,包含了超时控制、进度回调等高级功能:
javascript复制function promiseAllSettled(promises, options = {}) {
const {
timeout, // 全局超时时间
individualTimeout, // 单个Promise超时时间
onProgress, // 进度回调
} = options;
// 记录已完成的数量
let completedCount = 0;
// 处理超时逻辑
const createTimeoutPromise = (promise, index) => {
if (!individualTimeout && !timeout) return promise;
const timeoutMs = individualTimeout || timeout;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Promise ${index} timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
};
// 包装每个Promise
const wrappedPromises = promises.map((p, index) => {
const timedPromise = createTimeoutPromise(Promise.resolve(p), index);
return timedPromise
.then(value => {
completedCount++;
onProgress && onProgress(completedCount / promises.length);
return { status: 'fulfilled', value, index };
})
.catch(reason => {
completedCount++;
onProgress && onProgress(completedCount / promises.length);
return { status: 'rejected', reason, index };
});
});
// 处理全局超时
let settledPromise = Promise.all(wrappedPromises);
if (timeout) {
const globalTimeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Overall operation timed out after ${timeout}ms`));
}, timeout);
});
settledPromise = Promise.race([settledPromise, globalTimeoutPromise]);
}
return settledPromise;
}
3.3 TypeScript版本实现
对于使用TypeScript的项目,我们可以添加类型定义,使代码更加健壮:
typescript复制interface PromiseResult<T> {
status: 'fulfilled' | 'rejected';
value?: T;
reason?: any;
index?: number;
}
interface PromiseAllSettledOptions {
timeout?: number;
individualTimeout?: number;
onProgress?: (progress: number) => void;
}
async function promiseAllSettled<T>(
promises: Promise<T>[],
options: PromiseAllSettledOptions = {}
): Promise<PromiseResult<T>[]> {
// 实现逻辑与JavaScript版本相同
// ...
}
4. 使用场景与最佳实践
4.1 典型使用场景
- 批量API请求:当需要同时请求多个接口,且不希望因为某个接口失败而影响其他接口数据的获取时。
javascript复制const apiEndpoints = [
fetch('/api/user'),
fetch('/api/products'),
fetch('/api/orders')
];
promiseAllSettled(apiEndpoints, {
timeout: 5000,
onProgress: progress => console.log(`Progress: ${(progress * 100).toFixed(1)}%`)
})
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.error('Failed:', result.reason);
}
});
});
-
文件批量处理:处理多个文件上传或读取操作时。
-
数据库批量操作:执行多个数据库查询或更新操作时。
4.2 性能优化建议
- 控制并发数量:对于大量Promise,可以考虑分批次执行,避免一次性创建过多Promise导致内存问题。
javascript复制async function batchProcess(promises, batchSize = 10) {
const results = [];
for (let i = 0; i < promises.length; i += batchSize) {
const batch = promises.slice(i, i + batchSize);
const batchResults = await promiseAllSettled(batch);
results.push(...batchResults);
}
return results;
}
-
合理设置超时时间:根据实际网络状况和业务需求设置适当的超时时间。
-
利用进度回调:对于长时间运行的任务,使用onProgress回调提供用户反馈。
4.3 错误处理策略
- 分类处理错误:根据错误类型采取不同处理策略。
javascript复制promiseAllSettled(promises)
.then(results => {
const errors = results.filter(r => r.status === 'rejected');
// 处理网络错误
const networkErrors = errors.filter(e => e.reason instanceof NetworkError);
// 处理超时错误
const timeoutErrors = errors.filter(e => e.reason.message.includes('timed out'));
// 其他错误
const otherErrors = errors.filter(e => !(e.reason instanceof NetworkError) &&
!e.reason.message.includes('timed out'));
});
- 重试机制:对于失败的Promise,可以实现自动重试逻辑。
javascript复制function withRetry(promiseFn, retries = 3) {
return promiseFn().catch(err =>
retries > 0
? withRetry(promiseFn, retries - 1)
: Promise.reject(err)
);
}
5. 与其他方案的对比
5.1 与原生Promise.all的对比
| 特性 | 原生Promise.all | 封装版Promise.all |
|---|---|---|
| 单个失败是否导致整体失败 | 是 | 否 |
| 错误信息详细程度 | 简单 | 详细 |
| 超时控制 | 无 | 支持 |
| 进度反馈 | 无 | 支持 |
| 状态追踪 | 无 | 支持 |
5.2 与Promise.allSettled的对比
ES2020引入了Promise.allSettled,它与我们的封装版本有些相似,但也有区别:
- 超时控制:原生Promise.allSettled不支持超时控制
- 进度回调:原生版本不提供进度回调
- 索引信息:我们的实现包含了每个Promise的索引信息
javascript复制// 原生Promise.allSettled使用示例
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
});
6. 高级应用与扩展
6.1 实现优先级控制
我们可以扩展我们的实现,使其支持Promise的优先级控制:
javascript复制function promiseAllSettledWithPriority(promises, priorities, options = {}) {
// 根据优先级排序
const prioritized = promises.map((p, i) => ({ promise: p, priority: priorities[i] || 0 }))
.sort((a, b) => b.priority - a.priority);
return promiseAllSettled(prioritized.map(p => p.promise), options);
}
6.2 实现依赖关系管理
对于有依赖关系的Promise,我们可以实现依赖管理:
javascript复制async function resolveDependencies(promisesWithDeps) {
const results = [];
const resolved = new Set();
while (results.length < promisesWithDeps.length) {
const ready = promisesWithDeps.filter((p, i) =>
!resolved.has(i) &&
(!p.deps || p.deps.every(dep => resolved.has(dep)))
);
if (ready.length === 0) {
throw new Error('Circular dependency detected');
}
const batchResults = await promiseAllSettled(ready.map(p => p.promise));
batchResults.forEach((result, i) => {
const originalIndex = promisesWithDeps.indexOf(ready[i]);
results[originalIndex] = result;
resolved.add(originalIndex);
});
}
return results;
}
6.3 性能监控扩展
我们可以扩展实现,加入性能监控功能:
javascript复制function withPerfMonitoring(fn) {
return async function(...args) {
const start = performance.now();
const result = await fn(...args);
const duration = performance.now() - start;
return {
...result,
meta: {
...(result.meta || {}),
duration
}
};
};
}
const monitoredPromiseAllSettled = withPerfMonitoring(promiseAllSettled);
7. 常见问题与解决方案
7.1 内存泄漏问题
当处理大量Promise时,可能会遇到内存问题。解决方案:
- 分批次处理Promise
- 及时清理不再需要的引用
- 使用WeakMap存储元数据
javascript复制async function processInBatches(items, processFn, batchSize = 100) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(item => processFn(item));
const batchResults = await promiseAllSettled(batchPromises);
results.push(...batchResults);
// 强制垃圾回收(仅在Node.js环境)
if (global.gc) global.gc();
}
return results;
}
7.2 取消操作问题
JavaScript原生的Promise不支持取消操作,但我们可以模拟实现:
javascript复制function createCancelablePromise(promise) {
let cancel;
const cancelablePromise = new Promise((resolve, reject) => {
cancel = reject;
promise.then(resolve).catch(reject);
});
return {
promise: cancelablePromise,
cancel: (reason = 'Operation canceled') => cancel(new Error(reason))
};
}
function cancelablePromiseAllSettled(promises, options = {}) {
const cancelable = promises.map(createCancelablePromise);
const promise = promiseAllSettled(cancelable.map(c => c.promise), options);
return {
promise,
cancel: (reason) => cancelable.forEach(c => c.cancel(reason))
};
}
7.3 调试技巧
当Promise.all封装出现问题时,可以使用以下调试技巧:
- 记录每个Promise的创建和完成时间
- 为每个Promise添加唯一标识
- 使用AsyncLocalStorage(Node.js)或类似机制保持上下文
javascript复制function withDebugInfo(promise, id) {
const start = Date.now();
return promise
.then(value => {
console.log(`Promise ${id} succeeded after ${Date.now() - start}ms`);
return value;
})
.catch(err => {
console.error(`Promise ${id} failed after ${Date.now() - start}ms`, err);
throw err;
});
}
function debugPromiseAllSettled(promises, options = {}) {
const debugPromises = promises.map((p, i) =>
withDebugInfo(Promise.resolve(p), `#${i}`)
);
return promiseAllSettled(debugPromises, options);
}
8. 测试策略与示例
8.1 单元测试示例
使用Jest编写单元测试:
javascript复制describe('promiseAllSettled', () => {
test('should resolve all promises', async () => {
const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];
const results = await promiseAllSettled(promises);
expect(results).toEqual([
{ status: 'fulfilled', value: 1 },
{ status: 'fulfilled', value: 2 },
{ status: 'fulfilled', value: 3 }
]);
});
test('should handle rejected promises', async () => {
const promises = [
Promise.resolve(1),
Promise.reject(new Error('failed')),
Promise.resolve(3)
];
const results = await promiseAllSettled(promises);
expect(results).toEqual([
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: expect.any(Error) },
{ status: 'fulfilled', value: 3 }
]);
});
test('should respect timeout', async () => {
const promises = [
new Promise(resolve => setTimeout(() => resolve(1), 200)),
new Promise(resolve => setTimeout(() => resolve(2), 400))
];
await expect(promiseAllSettled(promises, { timeout: 300 }))
.rejects.toThrow('Overall operation timed out');
});
});
8.2 性能测试
使用Benchmark.js进行性能测试:
javascript复制const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;
suite
.add('Native Promise.all', {
defer: true,
fn: function(deferred) {
Promise.all([...promises])
.then(() => deferred.resolve())
.catch(() => deferred.resolve());
}
})
.add('Custom promiseAllSettled', {
defer: true,
fn: function(deferred) {
promiseAllSettled([...promises])
.then(() => deferred.resolve());
}
})
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ 'async': true });
8.3 E2E测试场景
模拟真实业务场景的测试:
javascript复制describe('API Batch Requests', () => {
test('should handle mixed success/failure', async () => {
// 模拟API请求函数
const mockApi = (success, delay, data) =>
new Promise((resolve, reject) =>
setTimeout(() =>
success ? resolve(data) : reject(new Error('API failed')),
delay
)
);
const apiCalls = [
mockApi(true, 100, { user: 'Alice' }),
mockApi(false, 200, null),
mockApi(true, 150, { user: 'Bob' })
];
const results = await promiseAllSettled(apiCalls, {
timeout: 300,
onProgress: p => console.log(`Progress: ${p * 100}%`)
});
expect(results).toHaveLength(3);
expect(results[0].status).toBe('fulfilled');
expect(results[1].status).toBe('rejected');
expect(results[2].status).toBe('fulfilled');
});
});
9. 在流行框架中的集成
9.1 React中的使用示例
在React组件中管理多个异步操作:
javascript复制function UserDashboard() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const results = await promiseAllSettled([
fetchUser(),
fetchPosts(),
fetchNotifications()
], {
timeout: 5000,
onProgress: p => console.log(`Loading ${(p * 100).toFixed(0)}%`)
});
const [user, posts, notifications] = results;
setData({
user: user.status === 'fulfilled' ? user.value : null,
posts: posts.status === 'fulfilled' ? posts.value : [],
notifications: notifications.status === 'fulfilled' ? notifications.value : []
});
setError(
results.filter(r => r.status === 'rejected').map(r => r.reason)
);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// 渲染逻辑...
}
9.2 Vue中的使用示例
在Vue组件中使用封装后的Promise.all:
javascript复制export default {
data() {
return {
user: null,
posts: [],
notifications: [],
loading: false,
errors: []
};
},
methods: {
async loadData() {
this.loading = true;
this.errors = [];
try {
const results = await promiseAllSettled([
this.fetchUser(),
this.fetchPosts(),
this.fetchNotifications()
]);
if (results[0].status === 'fulfilled') {
this.user = results[0].value;
} else {
this.errors.push('Failed to load user');
}
if (results[1].status === 'fulfilled') {
this.posts = results[1].value;
} else {
this.errors.push('Failed to load posts');
}
if (results[2].status === 'fulfilled') {
this.notifications = results[2].value;
} else {
this.errors.push('Failed to load notifications');
}
} finally {
this.loading = false;
}
}
},
created() {
this.loadData();
}
};
9.3 Node.js中的高级应用
在Node.js服务端使用增强版Promise.all处理数据库操作:
javascript复制async function getAggregatedUserData(userId) {
const [user, orders, payments, logs] = await promiseAllSettled([
User.findById(userId),
Order.find({ userId }),
Payment.find({ userId }),
Log.find({ userId }).limit(100)
], {
individualTimeout: 2000,
onProgress: p => {
metrics.record('user_data_load_progress', p);
}
});
return {
user: user.status === 'fulfilled' ? user.value : null,
orders: orders.status === 'fulfilled' ? orders.value : [],
payments: payments.status === 'fulfilled' ? payments.value : [],
logs: logs.status === 'fulfilled' ? logs.value : [],
errors: [user, orders, payments, logs]
.filter(r => r.status === 'rejected')
.map(r => r.reason)
};
}
10. 未来演进与替代方案
10.1 使用Async Hooks实现更精细的控制
在Node.js环境中,可以使用Async Hooks API跟踪Promise的执行:
javascript复制const asyncHooks = require('async_hooks');
function withAsyncTracking(promiseAllFn) {
return function trackedPromiseAll(promises, options = {}) {
const executionTimes = new Map();
const hook = asyncHooks.createHook({
init(asyncId, type, triggerAsyncId) {
if (type === 'PROMISE') {
executionTimes.set(asyncId, {
start: Date.now(),
trigger: triggerAsyncId
});
}
},
destroy(asyncId) {
executionTimes.delete(asyncId);
}
});
hook.enable();
return promiseAllFn(promises, options)
.then(results => {
hook.disable();
return {
results,
metrics: Array.from(executionTimes.values())
};
})
.catch(err => {
hook.disable();
throw err;
});
};
}
const trackedPromiseAllSettled = withAsyncTracking(promiseAllSettled);
10.2 使用Worker Threads分流处理
对于CPU密集型的Promise操作,可以使用Worker Threads分流:
javascript复制const { Worker } = require('worker_threads');
function runInWorker(promiseFn) {
return new Promise((resolve, reject) => {
const worker = new Worker(`
const { parentPort } = require('worker_threads');
(${promiseFn})()
.then(result => parentPort.postMessage({ status: 'fulfilled', result }))
.catch(error => parentPort.postMessage({ status: 'rejected', error }));
`, { eval: true });
worker.on('message', message => {
if (message.status === 'fulfilled') {
resolve(message.result);
} else {
reject(message.error);
}
worker.terminate();
});
worker.on('error', reject);
worker.on('exit', code => {
if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
});
});
}
function promiseAllSettledInWorkers(promises, options = {}) {
const workerPromises = promises.map(p =>
runInWorker(() => Promise.resolve(p))
);
return promiseAllSettled(workerPromises, options);
}
10.3 使用Observable实现响应式处理
对于需要更复杂流程控制的场景,可以考虑使用RxJS等Observable库:
javascript复制const { from, forkJoin } = require('rxjs');
const { map, catchError } = require('rxjs/operators');
function observableAllSettled(promises) {
return forkJoin(
promises.map(p =>
from(p).pipe(
map(value => ({ status: 'fulfilled', value })),
catchError(reason => [{ status: 'rejected', reason }])
)
)
);
}
// 使用示例
observableAllSettled([
Promise.resolve(1),
Promise.reject(new Error('fail')),
Promise.resolve(3)
]).subscribe({
next: results => console.log('Results:', results),
error: err => console.error('Error:', err),
complete: () => console.log('Complete')
});
