从‘地狱’到‘天堂’:手把手教你用Promise.all和Promise.race优化前端性能
从‘地狱’到‘天堂’手把手教你用Promise.all和Promise.race优化前端性能前端开发中异步操作无处不在。从简单的数据请求到复杂的文件处理异步编程已经成为现代前端开发的基石。然而随着应用复杂度的提升传统的回调函数嵌套方式逐渐暴露出可维护性差、代码冗余等问题这就是所谓的回调地狱。Promise的出现为我们提供了一种更优雅的解决方案而Promise.all和Promise.race这两个高阶方法更是将异步编程提升到了新的高度。本文将深入探讨如何利用Promise.all和Promise.race来优化前端性能通过实际案例展示它们在不同场景下的应用技巧。无论你是正在处理多个并行接口请求还是需要实现竞速逻辑来提升用户体验这些技术都能让你的代码更加高效、健壮。1. 理解Promise的核心机制在深入探讨Promise.all和Promise.race之前我们需要先理解Promise的基本工作原理。Promise本质上是一个表示异步操作最终完成或失败的对象它有三种状态pending进行中、fulfilled已成功和rejected已失败。Promise的核心优势在于它提供了链式调用的能力通过.then()方法可以将多个异步操作串联起来避免了传统的回调地狱问题。例如fetch(/api/data) .then(response response.json()) .then(data processData(data)) .catch(error handleError(error));这种链式调用不仅使代码更加清晰还提供了统一的错误处理机制。但真正的威力在于Promise的并行处理能力这正是Promise.all和Promise.race发挥作用的地方。2. Promise.all并行处理的利器Promise.all方法接收一个Promise数组作为参数返回一个新的Promise。这个新Promise会在所有输入的Promise都成功完成时resolve或者在任何一个Promise失败时reject。2.1 基础用法与性能优势const promise1 fetch(/api/user); const promise2 fetch(/api/products); const promise3 fetch(/api/cart); Promise.all([promise1, promise2, promise3]) .then(([user, products, cart]) { // 所有请求都已完成 renderPage(user, products, cart); }) .catch(error { console.error(其中一个请求失败:, error); });这种方式的性能优势显而易见三个原本需要串行执行的请求现在可以并行发起大大减少了总等待时间。在需要同时获取多个不相关数据的场景下这种优化可以显著提升页面加载速度。2.2 实际应用场景场景一首屏数据加载优化现代前端应用通常需要在首屏加载多种数据用户信息、产品列表、购物车内容等。使用Promise.all可以并行请求这些数据而不是一个接一个地等待。async function loadInitialData() { try { const [user, products, recommendations] await Promise.all([ fetchUser(), fetchProducts(), fetchRecommendations() ]); // 更新UI updateUserProfile(user); renderProductList(products); showRecommendations(recommendations); } catch (error) { showErrorToast(加载数据失败请重试); } }场景二批量文件上传当需要上传多个文件时Promise.all可以确保所有文件都上传完成后再进行后续处理function uploadFiles(files) { const uploadPromises files.map(file { return uploadSingleFile(file); }); return Promise.all(uploadPromises) .then(results { console.log(所有文件上传成功, results); return results; }); }2.3 高级技巧与注意事项错误处理策略Promise.all是全有或全无的模式任何一个Promise失败都会导致整个操作失败。如果希望即使部分失败也能获取其他成功的结果可以考虑以下方案Promise.all( promises.map(p p.catch(e ({ error: e }))) ).then(results { // 检查每个结果是否有error属性 });内存考虑当处理大量Promise时要注意内存消耗。可以考虑使用分批次处理async function processInBatches(promises, batchSize) { const results []; for (let i 0; i promises.length; i batchSize) { const batch promises.slice(i, i batchSize); const batchResults await Promise.all(batch); results.push(...batchResults); } return results; }3. Promise.race竞速逻辑的实现与Promise.all不同Promise.race会在数组中任何一个Promise完成无论成功或失败时就立即返回结果。这种特性使其非常适合实现超时控制、竞速获取资源等场景。3.1 基础用法const promise1 new Promise((resolve) setTimeout(() resolve(结果1), 500)); const promise2 new Promise((resolve) setTimeout(() resolve(结果2), 200)); Promise.race([promise1, promise2]) .then(result { console.log(result); // 输出结果2因为它更快 });3.2 实际应用场景场景一请求超时控制function fetchWithTimeout(url, timeout 5000) { const fetchPromise fetch(url); const timeoutPromise new Promise((_, reject) setTimeout(() reject(new Error(请求超时)), timeout)); return Promise.race([fetchPromise, timeoutPromise]); } // 使用示例 fetchWithTimeout(/api/data) .then(response response.json()) .catch(error { if (error.message 请求超时) { showTimeoutMessage(); } else { handleOtherErrors(error); } });场景二多CDN资源竞速function getFastestResource(resources) { const promises resources.map(resource fetch(resource).then(response ({ url: resource, response })) ); return Promise.race(promises) .then(({ url, response }) { // 缓存最快的资源URL供后续使用 cacheFastestResource(url); return response; }); } // 使用示例 const CDNS [ https://cdn1.example.com/data.json, https://cdn2.example.com/data.json, https://cdn3.example.com/data.json ]; getFastestResource(CDNS) .then(response response.json()) .then(data useData(data));3.3 高级技巧取消慢请求结合AbortController可以取消已经不需要的慢请求function fetchWithCancelRace(urls) { const controllers urls.map(() new AbortController()); const promises urls.map((url, index) { const { signal } controllers[index]; return fetch(url, { signal }) .then(response { // 取消其他请求 controllers.forEach((c, i) i ! index c.abort()); return response; }); }); return Promise.race(promises); }首屏关键资源优先对于首屏渲染可以同时请求关键资源和非关键资源但只等待关键资源async function loadPage() { const criticalResource fetch(/api/critical); const nonCriticalResource fetch(/api/non-critical); // 只等待关键资源 const criticalData await Promise.race([ criticalResource, // 设置一个合理的最长等待时间 new Promise(resolve setTimeout(() resolve(null), 1000)) ]); renderCriticalContent(criticalData); // 非关键资源在后台继续加载 nonCriticalResource.then(data { if (data) renderNonCriticalContent(data); }); }4. 结合Vue3的实战应用Vue3的Composition API与Promise的结合可以创造出非常强大的数据获取模式。下面我们来看几个在Vue3中使用Promise.all和Promise.race的实际例子。4.1 组合式数据获取import { ref, onMounted } from vue; export default { setup() { const user ref(null); const products ref([]); const recommendations ref([]); const loading ref(true); const error ref(null); onMounted(async () { try { [user.value, products.value, recommendations.value] await Promise.all([ fetchUser(), fetchProducts(), fetchRecommendations() ]); } catch (err) { error.value err; } finally { loading.value false; } }); return { user, products, recommendations, loading, error }; } };4.2 竞速加载与回退策略import { shallowRef, onMounted } from vue; export default { setup() { const data shallowRef(null); const loading ref(true); onMounted(async () { const primarySource fetchPrimaryData().then(r ({ source: primary, data: r })); const fallbackSource fetchFallbackData().then(r ({ source: fallback, data: r })); try { const result await Promise.race([ primarySource, fallbackSource, // 超时回退 new Promise((_, reject) setTimeout(() reject(new Error(timeout)), 1000)) ]); data.value result.data; if (result.source fallback) { logFallbackUsage(); } } catch (err) { if (err.message timeout) { data.value getCachedData(); } else { handleError(err); } } finally { loading.value false; } }); return { data, loading }; } };4.3 性能优化技巧请求合并对于可能重复的请求可以在发出前进行合并const pendingRequests new Map(); function dedupedFetch(url) { if (pendingRequests.has(url)) { return pendingRequests.get(url); } const promise fetch(url) .finally(() pendingRequests.delete(url)); pendingRequests.set(url, promise); return promise; } async function loadMultipleResources(urls) { const uniqueUrls [...new Set(urls)]; const responses await Promise.all(uniqueUrls.map(dedupedFetch)); // 处理响应... }分优先级加载结合Promise.race实现关键资源优先加载function loadWithPriority(highPriorityUrls, lowPriorityUrls) { const highPriority Promise.all(highPriorityUrls.map(fetch)); const lowPriority Promise.all(lowPriorityUrls.map(fetch)); // 立即开始加载高优先级资源 highPriority.then(renderCriticalContent); // 当高优先级资源加载完成后再处理低优先级 highPriority.then(() { lowPriority.then(renderNonCriticalContent); }); // 如果高优先级加载太慢至少显示一些内容 Promise.race([ highPriority, new Promise(resolve setTimeout(resolve, 1000)) ]).then(ensureMinimumContent); }