1. 异步请求的常见场景与挑战
在前端开发中,我们经常会遇到需要同时发起多个异步请求的场景。比如一个电商网站的商品详情页,可能需要同时获取商品基本信息、库存状态、用户评价、推荐商品等多个接口的数据。这种情况下,如何优雅地处理多个异步请求的结果,就成了一个值得深入探讨的问题。
我最近在重构一个老项目时,就遇到了这样一个典型需求:系统需要同时向三个不同的供应商API发起查询请求,只要任意一个供应商返回了有效结果,就立即展示给用户;只有当所有供应商都查询失败时,才显示错误提示。这种"任一成功则继续,全部失败才报错"的逻辑,在分布式系统和高可用场景中非常常见。
2. 基础方案:Promise.all与Promise.any的局限
2.1 Promise.all的不足
很多开发者首先想到的可能是Promise.all。这个方法确实可以处理多个Promise,但它的问题是必须等待所有Promise都完成(无论成功或失败)才会返回结果。这不符合我们"任一成功就继续"的需求。
// 不适用场景的Promise.all示例 const promises = [fetchApi1(), fetchApi2(), fetchApi3()]; Promise.all(promises) .then((results) => { // 所有请求都成功才会进入这里 }) .catch((error) => { // 只要有一个失败就会进入这里 });2.2 Promise.any的适用性分析
ES2021引入了Promise.any,它正好满足"任一成功就继续"的需求:
const promises = [fetchApi1(), fetchApi2(), fetchApi3()]; Promise.any(promises) .then((firstSuccess) => { // 任一请求成功就进入这里 }) .catch((errors) => { // 所有请求都失败才会进入这里 });但实际项目中我们需要注意几个问题:
- 兼容性问题:Promise.any是比较新的API,在不支持的环境中需要polyfill
- 错误处理:我们需要收集所有失败的原因,而不只是第一个错误
- 取消逻辑:当某个请求成功后,其他仍在进行的请求是否需要取消
3. 手写实现核心逻辑
3.1 基础实现方案
下面是一个兼容性更好的手写实现,不依赖Promise.any:
function anySuccess(promises) { return new Promise((resolve, reject) => { let rejectedCount = 0; const errors = []; promises.forEach((promise, index) => { promise.then( (result) => { resolve(result); // 任一成功立即resolve }, (error) => { errors[index] = error; rejectedCount++; if (rejectedCount === promises.length) { reject(new AggregateError(errors, "All promises were rejected")); } } ); }); }); }3.2 进阶优化版本
实际项目中我们还需要考虑更多边界情况:
function anySuccessWithOptions(promises, options = {}) { const { shouldAbort = true, // 是否取消其他请求 timeout, // 超时设置 collectErrors = true // 是否收集所有错误 } = options; return new Promise((resolve, reject) => { let rejectedCount = 0; const errors = new Array(promises.length); const controllers = []; // 处理超时逻辑 let timeoutId; if (timeout) { timeoutId = setTimeout(() => { controllers.forEach(c => c && c.abort()); reject(new Error(`Timeout after ${timeout}ms`)); }, timeout); } promises.forEach((promise, index) => { // 支持传入函数延迟创建Promise const actualPromise = typeof promise === 'function' ? promise() : promise; // 记录AbortController以便后续取消 let controller; if (shouldAbort && actualPromise instanceof Promise) { controller = new AbortController(); controllers[index] = controller; } actualPromise.then( (result) => { clearTimeout(timeoutId); if (shouldAbort) { controllers.forEach((c, i) => { if (i !== index && c) c.abort(); }); } resolve(result); }, (error) => { if (collectErrors) errors[index] = error; rejectedCount++; if (rejectedCount === promises.length) { clearTimeout(timeoutId); reject(collectErrors ? new AggregateError(errors, "All promises were rejected") : new Error("All promises were rejected") ); } } ); }); }); }4. 实际应用中的注意事项
4.1 性能优化技巧
- 差异化超时设置:对不同稳定性的API设置不同的超时时间
- 请求优先级:重要的API先发起请求
- 结果缓存:对部分请求结果进行缓存,减少重复请求
// 示例:差异化超时 const apis = [ { request: fetchStableApi, timeout: 3000 }, { request: fetchUnstableApi, timeout: 1000 } ]; const promises = apis.map(api => { return Promise.race([ api.request(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), api.timeout) ) ]); });4.2 错误处理最佳实践
- 错误分类:区分网络错误、业务错误、超时错误等
- 错误上报:收集所有错误信息用于监控分析
- 优雅降级:根据错误类型提供不同的降级方案
anySuccessWithOptions(promises, { collectErrors: true }).catch(aggregateError => { // 上报所有错误 reportErrors(aggregateError.errors); // 根据错误类型提供不同降级方案 if (allErrorsAreTimeout(aggregateError.errors)) { showTimeoutMessage(); } else { showGenericError(); } });5. 在React/Vue中的实践
5.1 React示例:自定义Hook实现
import { useState, useEffect } from 'react'; function useAnySuccess(promises, options) { const [result, setResult] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { let isMounted = true; setIsLoading(true); anySuccessWithOptions(promises, options) .then(res => { if (isMounted) { setResult(res); setError(null); setIsLoading(false); } }) .catch(err => { if (isMounted) { setError(err); setResult(null); setIsLoading(false); } }); return () => { isMounted = false; }; }, [promises, options]); return { result, error, isLoading }; }5.2 Vue示例:组合式函数实现
import { ref, watchEffect } from 'vue'; export function useAnySuccess(promises, options) { const result = ref(null); const error = ref(null); const isLoading = ref(true); watchEffect((onCleanup) => { let isActive = true; isLoading.value = true; anySuccessWithOptions(promises, options) .then(res => { if (isActive) { result.value = res; error.value = null; isLoading.value = false; } }) .catch(err => { if (isActive) { error.value = err; result.value = null; isLoading.value = false; } }); onCleanup(() => { isActive = false; }); }); return { result, error, isLoading }; }6. Node.js服务端应用
在服务端场景下,我们还需要考虑:
- 请求重试机制:对失败的请求进行有限次重试
- 负载均衡:在多个服务实例间轮询
- 熔断机制:当错误率达到阈值时暂时停止请求
async function withRetry(promiseFn, maxRetries = 3) { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await promiseFn(); } catch (error) { lastError = error; await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)) // 指数退避 ); } } throw lastError; } // 使用示例 const services = [ () => withRetry(() => callServiceA()), () => withRetry(() => callServiceB()) ]; anySuccessWithOptions(services);7. 测试策略与调试技巧
7.1 单元测试要点
describe('anySuccess', () => { it('should resolve when first promise resolves', async () => { const promises = [ Promise.reject(new Error('fail1')), Promise.resolve('success'), new Promise(() => {}) // pending ]; await expect(anySuccess(promises)).resolves.toBe('success'); }); it('should reject with all errors when all promises reject', async () => { const error1 = new Error('fail1'); const error2 = new Error('fail2'); const promises = [ Promise.reject(error1), Promise.reject(error2) ]; await expect(anySuccess(promises)).rejects.toSatisfy(err => { return err.errors[0] === error1 && err.errors[1] === error2; }); }); });7.2 调试技巧
- 请求追踪:为每个请求添加唯一ID方便追踪
- 性能分析:记录每个请求的耗时
- Mock数据:使用工具模拟不同响应场景
function tracedFetch(url, options = {}) { const requestId = generateId(); const startTime = Date.now(); console.log(`[${requestId}] Starting request to ${url}`); return fetch(url, options) .then(response => { console.log(`[${requestId}] Request succeeded in ${Date.now() - startTime}ms`); return response; }) .catch(error => { console.error(`[${requestId}] Request failed after ${Date.now() - startTime}ms`, error); throw error; }); }在实际项目中实现多个异步请求的统一处理时,我发现最重要的不是选择最炫技的实现方式,而是确保代码的可维护性和可调试性。为此我总结了几条经验:
- 清晰的错误日志:确保每个失败的请求都能留下足够的排查线索
- 合理的超时设置:根据业务特点设置不同的超时阈值
- 适当的监控指标:统计成功率、响应时间等关键指标
- 完善的文档注释:说明每个参数的作用和边界情况
这些经验看似简单,但在复杂的生产环境中,往往比精巧的代码实现更能提高系统的可靠性。