1. 异步编程与并行执行的核心概念
在JavaScript的世界里,异步编程就像是在餐厅同时处理多桌客人的点单。传统的同步执行方式相当于服务员必须等一桌客人完全点完并上菜后,才能服务下一桌。而异步编程则允许服务员在等待厨房准备菜品的同时,继续为其他客人服务。
Promise是ES6引入的异步编程解决方案,它代表一个尚未完成但预期将来会完成的操作。想象你网购了一件商品,商家会给你一个快递单号(Promise),这个单号不是商品本身,但代表着未来某个时间你会收到商品。
const fetchData = new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => resolve('数据加载成功'), 1000); }); fetchData.then(data => console.log(data));2. 并行执行异步函数的实现方案
2.1 Promise.all的基础用法
Promise.all就像是在等待多个快递同时送达。只有当所有包裹都到齐时,你才会开始拆箱。如果其中任何一个包裹丢失(reject),整个等待过程就会立即终止。
const promise1 = Promise.resolve(3); const promise2 = 42; const promise3 = new Promise((resolve) => { setTimeout(resolve, 100, 'foo'); }); Promise.all([promise1, promise2, promise3]).then((values) => { console.log(values); // [3, 42, "foo"] });2.2 错误处理与容错机制
在实际开发中,我们需要考虑网络请求失败的情况。就像快递可能丢失一样,我们需要有备选方案:
const promises = [ fetch('/api/data1').catch(e => ({ error: e })), fetch('/api/data2').catch(e => ({ error: e })), fetch('/api/data3').catch(e => ({ error: e })) ]; Promise.all(promises).then(results => { const successfulResults = results.filter(r => !r.error); const failedRequests = results.filter(r => r.error); console.log('成功获取:', successfulResults); if(failedRequests.length) { console.warn('以下请求失败:', failedRequests); } });3. 高级并行控制技巧
3.1 限制并发数量的实现
有时候我们需要控制同时进行的异步任务数量,就像餐厅只有有限的厨师,不能同时处理所有订单:
async function parallelWithLimit(tasks, limit) { const results = []; const executing = new Set(); for (const task of tasks) { const p = task().then(result => { executing.delete(p); return result; }); executing.add(p); results.push(p); if (executing.size >= limit) { await Promise.race(executing); } } return Promise.all(results); } // 使用示例 const tasks = [ () => fetch('/api/1'), () => fetch('/api/2'), // ...更多任务 ]; parallelWithLimit(tasks, 3).then(console.log);3.2 带优先级的并行执行
某些情况下,我们需要优先处理重要任务:
class PriorityPromise { constructor(task, priority = 0) { this.task = task; this.priority = priority; } execute() { return this.task(); } } async function runWithPriority(tasks) { const queue = [...tasks].sort((a, b) => b.priority - a.priority); const results = []; for (const item of queue) { results.push(await item.execute()); } return results; }4. 力扣2721题的实战解析
4.1 题目要求分析
力扣2721题要求我们实现一个parallel函数,能够并行执行一组异步函数,并返回一个Promise,其结果为这些异步函数返回值的数组。这与Promise.all的功能类似,但需要我们自己实现。
4.2 基础实现方案
function parallel(functions) { return new Promise((resolve) => { const results = []; let completed = 0; functions.forEach((fn, index) => { fn().then(result => { results[index] = result; completed++; if (completed === functions.length) { resolve(results); } }); }); }); }4.3 优化版本实现
我们可以进一步优化,添加错误处理和更高效的完成检测:
function parallel(functions) { return new Promise((resolve, reject) => { if (functions.length === 0) return resolve([]); const results = new Array(functions.length); let remaining = functions.length; functions.forEach((fn, index) => { Promise.resolve(fn()) .then(result => { results[index] = result; remaining--; if (remaining === 0) { resolve(results); } }) .catch(reject); }); }); }5. 性能优化与调试技巧
5.1 内存管理注意事项
并行执行大量异步任务时,需要注意内存使用:
// 不好的做法 - 一次性加载所有数据 async function loadAllData() { const allData = await Promise.all(hugeArray.map(fetchData)); // 处理数据... } // 更好的做法 - 分批处理 async function batchProcess(array, batchSize = 10) { for (let i = 0; i < array.length; i += batchSize) { const batch = array.slice(i, i + batchSize); const results = await Promise.all(batch.map(fetchData)); // 处理当前批次... } }5.2 调试异步代码的技巧
调试并行执行的代码可能会遇到一些挑战:
- 使用console.time和console.timeEnd测量执行时间
- 为每个Promise添加唯一标识符便于追踪
- 在catch块中输出详细的错误信息
function createTrackedPromise(fn, id) { console.time(id); return fn() .then(result => { console.timeEnd(id); return result; }) .catch(err => { console.error(`Error in ${id}:`, err); throw err; }); } const trackedPromises = tasks.map((task, i) => createTrackedPromise(task, `Task-${i}`) );6. 实际应用场景分析
6.1 前端数据预加载
在单页应用中,我们可以并行加载多个路由所需的初始数据:
async function preloadRouteData(routes) { const dataPromises = routes.map(route => { return fetch(route.dataUrl) .then(res => res.json()) .then(data => ({ route, data })); }); return Promise.all(dataPromises); } // 使用 preloadRouteData([ { path: '/users', dataUrl: '/api/users' }, { path: '/products', dataUrl: '/api/products' } ]).then(preloadedData => { // 建立路由到预加载数据的映射 const routeDataMap = new Map( preloadedData.map(({ route, data }) => [route.path, data]) ); });6.2 服务端批量处理
在Node.js服务端,我们可以并行处理多个独立的数据操作:
async function processUserBatch(userIds) { const userPromises = userIds.map(async userId => { const [profile, orders, preferences] = await Promise.all([ fetchProfile(userId), fetchOrders(userId), fetchPreferences(userId) ]); return { userId, profile, orders, preferences }; }); return Promise.all(userPromises); }7. 常见问题与解决方案
7.1 内存泄漏问题
并行执行大量Promise时,如果处理不当可能会导致内存泄漏:
// 潜在的内存泄漏示例 let pendingPromises = []; function startTask(data) { const promise = processData(data) .then(result => { // 从数组中移除已完成的Promise pendingPromises = pendingPromises.filter(p => p !== promise); return result; }); pendingPromises.push(promise); return promise; } // 更好的做法是使用WeakMap const pendingPromises = new WeakMap(); function startTask(data) { const promise = processData(data); pendingPromises.set(promise, true); promise.finally(() => { pendingPromises.delete(promise); }); return promise; }7.2 未捕获的Promise异常
未处理的Promise拒绝会导致难以调试的问题:
// 不好的做法 - 未处理的拒绝 function riskyOperation() { return new Promise((resolve, reject) => { // 可能失败的异步操作 }); } // 好的做法 - 始终处理错误 function safeOperation() { return new Promise((resolve, reject) => { // 异步操作 }).catch(err => { console.error('Operation failed:', err); throw err; // 可以选择重新抛出或返回默认值 }); } // 全局捕获未处理的Promise拒绝 process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); });8. 进阶话题:取消Promise执行
有时候我们需要中止正在进行的异步操作:
function cancellablePromise(executor) { let cancel; const promise = new Promise((resolve, reject) => { cancel = (reason = 'Cancelled') => { reject(new Error(reason)); }; executor(resolve, reject, () => cancel); }); promise.cancel = cancel; return promise; } // 使用示例 const fetchWithCancel = cancellablePromise((resolve, reject, onCancel) => { const timer = setTimeout(() => resolve('Data loaded'), 2000); onCancel(() => { clearTimeout(timer); console.log('Request cancelled'); }); }); // 取消请求 setTimeout(() => fetchWithCancel.cancel(), 1000);9. 测试并行代码的策略
测试异步代码需要特别注意:
// 使用Jest测试并行代码 describe('parallel function', () => { it('should execute functions in parallel', async () => { const mockFn1 = jest.fn(() => new Promise(resolve => setTimeout(() => resolve(1), 100)) ); const mockFn2 = jest.fn(() => new Promise(resolve => setTimeout(() => resolve(2), 50)) ); const start = Date.now(); const results = await parallel([mockFn1, mockFn2]); const duration = Date.now() - start; expect(results).toEqual([1, 2]); expect(duration).toBeLessThan(150); // 并行执行应快于顺序执行 expect(mockFn1).toHaveBeenCalled(); expect(mockFn2).toHaveBeenCalled(); }); it('should maintain order of results', async () => { const fastFn = () => Promise.resolve('fast'); const slowFn = () => new Promise(resolve => setTimeout(() => resolve('slow'), 100) ); const results = await parallel([slowFn, fastFn]); expect(results).toEqual(['slow', 'fast']); // 结果顺序与输入顺序一致 }); });10. 浏览器与Node.js环境差异
在不同JavaScript运行时中,并行执行可能表现不同:
// 浏览器中的Web Workers并行处理 function runInWorker(code) { const blob = new Blob([code], { type: 'application/javascript' }); const url = URL.createObjectURL(blob); return new Worker(url); } // Node.js中使用worker_threads const { Worker } = require('worker_threads'); function runInNodeWorker(filePath, data) { return new Promise((resolve, reject) => { const worker = new Worker(filePath, { workerData: data }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } }); }); }