异步串行 | 异步并行

实现异步任务的串行执行和并行执行,掌握 Promise 控制流程的技巧

问题

在实际开发中,我们经常需要处理多个异步任务。根据业务场景不同,有时需要串行执行(一个接一个按顺序执行),有时需要并行执行(同时执行所有任务)。本题要求实现这两种异步任务的执行方式。

串行执行:任务按顺序依次执行,前一个任务完成后才执行下一个任务。 并行执行:所有任务同时开始执行,等待所有任务完成。

解答

/**
 * 异步串行执行
 * @param {Array<Function>} tasks - 异步任务数组,每个任务返回 Promise
 * @returns {Promise<Array>} 返回所有任务结果的数组
 */
function serialExecute(tasks) {
  return tasks.reduce((promiseChain, currentTask) => {
    return promiseChain.then(results => {
      return currentTask().then(result => {
        return [...results, result];
      });
    });
  }, Promise.resolve([]));
}

// 串行执行的另一种实现方式(使用 async/await)
async function serialExecuteAsync(tasks) {
  const results = [];
  for (const task of tasks) {
    const result = await task();
    results.push(result);
  }
  return results;
}

/**
 * 异步并行执行
 * @param {Array<Function>} tasks - 异步任务数组,每个任务返回 Promise
 * @returns {Promise<Array>} 返回所有任务结果的数组
 */
function parallelExecute(tasks) {
  return Promise.all(tasks.map(task => task()));
}

// 并行执行的另一种实现方式(手动实现 Promise.all)
function parallelExecuteManual(tasks) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completedCount = 0;
    const totalTasks = tasks.length;

    if (totalTasks === 0) {
      resolve(results);
      return;
    }

    tasks.forEach((task, index) => {
      task()
        .then(result => {
          results[index] = result; // 保持结果顺序
          completedCount++;
          
          if (completedCount === totalTasks) {
            resolve(results);
          }
        })
        .catch(error => {
          reject(error); // 任意一个失败就拒绝
        });
    });
  });
}

使用示例

// 模拟异步任务
const createTask = (name, delay, shouldFail = false) => {
  return () => {
    return new Promise((resolve, reject) => {
      console.log(`${name} 开始执行`);
      setTimeout(() => {
        if (shouldFail) {
          console.log(`${name} 执行失败`);
          reject(new Error(`${name} failed`));
        } else {
          console.log(`${name} 执行完成`);
          resolve(`${name} 的结果`);
        }
      }, delay);
    });
  };
};

// 创建测试任务
const task1 = createTask('任务1', 1000);
const task2 = createTask('任务2', 500);
const task3 = createTask('任务3', 800);

// 测试串行执行
console.log('=== 串行执行测试 ===');
serialExecute([task1, task2, task3])
  .then(results => {
    console.log('串行执行结果:', results);
    // 输出顺序:任务1 -> 任务2 -> 任务3
    // 总耗时:约 2300ms (1000 + 500 + 800)
  })
  .catch(error => {
    console.error('串行执行失败:', error);
  });

// 测试并行执行
setTimeout(() => {
  console.log('\n=== 并行执行测试 ===');
  parallelExecute([task1, task2, task3])
    .then(results => {
      console.log('并行执行结果:', results);
      // 所有任务同时开始
      // 总耗时:约 1000ms (取最长的任务时间)
    })
    .catch(error => {
      console.error('并行执行失败:', error);
    });
}, 3000);

// 实际应用场景示例
async function fetchUserData() {
  // 串行:必须先获取用户信息,再根据用户ID获取订单
  const userTasks = [
    () => fetch('/api/user').then(r => r.json()),
    () => fetch('/api/orders').then(r => r.json())
  ];
  const [user, orders] = await serialExecuteAsync(userTasks);
  
  // 并行:同时获取多个独立的数据
  const parallelTasks = [
    () => fetch('/api/products').then(r => r.json()),
    () => fetch('/api/categories').then(r => r.json()),
    () => fetch('/api/banners').then(r => r.json())
  ];
  const [products, categories, banners] = await parallelExecute(parallelTasks);
  
  return { user, orders, products, categories, banners };
}

关键点

  • 串行执行思路:使用 reduce 构建 Promise 链,或使用 async/await 配合 for...of 循环,确保前一个任务完成后再执行下一个

  • 并行执行思路:使用 Promise.all 同时启动所有任务,等待全部完成;或手动实现计数器机制追踪完成状态

  • 结果顺序保证:并行执行时要注意保持结果数组的顺序与任务数组一致,使用索引而非 push 来存储结果

  • 错误处理:串行执行遇到错误会中断后续任务;并行执行任意一个失败会导致整体失败(可使用 Promise.allSettled 改进)

  • 性能差异:串行执行时间是所有任务时间之和;并行执行时间取决于最慢的任务

  • 应用场景:串行适用于有依赖关系的任务(如先登录再获取数据);并行适用于独立任务(如同时加载多个资源)

  • 扩展方案:可实现带并发限制的并行执行(如最多同时执行 3 个任务),结合两者优势