JS如何顺序执行10个异步任务?—首选async/await + for 循环

73 阅读3分钟

在 JavaScript 中,顺序执行多个异步任务(如接口请求、文件读取、定时器等)的核心是:前一个任务完成后,再执行下一个任务。以下是 4 种常用方案,按 “简洁度 + 实用性” 排序:

一、最推荐:async/await(清晰直观,现代首选)

async/await 是 ES8 语法,本质是 Promise 的语法糖,能把异步代码写得像同步代码一样直观,是顺序执行异步任务的最优方案。

核心逻辑:

  1. 用 async 声明异步函数。
  2. 用 await 等待每个异步任务完成,再执行下一个。

示例(10 个异步任务顺序执行):

// 模拟异步任务(如接口请求,延迟 500ms 完成)
function asyncTask(index) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`任务 ${index + 1} 完成`);
      resolve(`结果 ${index + 1}`); // 任务完成后返回结果
    }, 500);
  });
}

// 顺序执行 10 个异步任务
async function runTasksInOrder() {
  const results = []; // 存储所有任务结果
  for (let i = 0; i < 10; i++) {
    const result = await asyncTask(i); // 等待当前任务完成
    results.push(result); // 收集结果
  }
  console.log("所有任务顺序执行完毕", results);
}

runTasksInOrder();

特点:

  • 代码简洁、可读性强,逻辑和同步代码一致。
  • 支持错误捕获(用 try/catch 包裹 await 语句)。
  • 可随时收集每个任务的结果,灵活处理。

错误处理示例:

async function runTasksInOrder() {
  const results = [];
  for (let i = 0; i < 10; i++) {
    try {
      const result = await asyncTask(i);
      results.push(result);
    } catch (err) {
      console.error(`任务 ${i + 1} 失败:`, err);
      // 可选:失败后继续执行下一个任务,或中断(return)
    }
  }
  console.log("所有任务执行完毕", results);
}

二、Promise 链式调用(then() 链式,兼容旧环境)

在 async/await 出现前,Promise 链式调用是顺序执行异步任务的标准方案。核心是:每个 then() 的回调返回下一个异步任务,形成链式依赖

示例:

// 模拟异步任务(同上)
function asyncTask(index) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`任务 ${index + 1} 完成`);
      resolve(`结果 ${index + 1}`);
    }, 500);
  });
}

// 链式调用顺序执行 10 个任务
function runTasksWithPromiseChain() {
  let promiseChain = Promise.resolve(); // 初始 Promise(已完成)
  const results = [];

  for (let i = 0; i < 10; i++) {
    // 每个 then() 等待前一个任务完成,再执行下一个
    promiseChain = promiseChain.then(() => {
      return asyncTask(i).then((result) => {
        results.push(result);
      });
    });
  }

  // 所有任务完成后执行
  promiseChain.then(() => {
    console.log("所有任务顺序执行完毕", results);
  });
}

runTasksWithPromiseChain();

特点:

  • 无需 async/await,兼容 ES6 环境。
  • 逻辑依赖链式结构,任务数量多时,代码嵌套略繁琐(不如 async/await 直观)。
  • 错误处理:在链式末尾加 catch() 捕获所有任务的错误,或在每个 then() 后加 catch() 单独处理。

三、递归调用(手动控制执行顺序)

核心逻辑:定义递归函数,执行当前任务后,递归调用自身执行下一个任务,直到所有任务完成

示例:

// 模拟异步任务(同上)
function asyncTask(index) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`任务 ${index + 1} 完成`);
      resolve(`结果 ${index + 1}`);
    }, 500);
  });
}

// 递归顺序执行
function runTasksWithRecursion() {
  const results = [];
  const totalTasks = 10;

  // 递归函数:执行第 n 个任务
  function runTask(n) {
    if (n >= totalTasks) {
      // 所有任务完成
      console.log("所有任务顺序执行完毕", results);
      return;
    }

    // 执行当前任务,完成后递归执行下一个
    asyncTask(n)
      .then((result) => {
        results.push(result);
        runTask(n + 1); // 执行下一个任务
      })
      .catch((err) => {
        console.error(`任务 ${n + 1} 失败:`, err);
        runTask(n + 1); // 失败后继续执行,或 return 中断
      });
  }

  runTask(0); // 从第 1 个任务开始(索引 0)
}

runTasksWithRecursion();

特点:

  • 无需循环,通过递归控制顺序,适合动态生成任务列表的场景。
  • 代码逻辑清晰,但需注意递归深度(10 个任务完全无压力,百级以上也无需担心栈溢出,因为是异步递归)。

四、Promise.all() 改造(不推荐,仅作对比)

Promise.all() 本身是并行执行所有任务,但如果强行用它顺序执行(每次只传一个任务),本质是 then() 链式的变体,代码冗余,不推荐使用。

不推荐示例(仅作对比):

function runTasksWithAll() {
  const results = [];
  let index = 0;

  function nextTask() {
    if (index >= 10) {
      console.log("所有任务顺序执行完毕", results);
      return;
    }

    // 每次只执行一个任务,完成后执行下一个
    Promise.all([asyncTask(index)])
      .then(([result]) => {
        results.push(result);
        index++;
        nextTask();
      });
  }

  nextTask();
}

特点:

  • 完全浪费 Promise.all() 并行的优势,代码冗余,不推荐。

核心对比与最佳实践

方案优点缺点适用场景
async/await + for代码简洁、直观,易调试依赖 ES8 环境(需转译旧浏览器)现代项目(React/Vue/Node)
Promise 链式兼容 ES6,无需转译任务多时有嵌套感,可读性一般旧环境或无 async/await 场景
递归调用动态任务列表友好,逻辑清晰需手动写递归函数,略繁琐动态生成任务(如接口返回任务列表)
Promise.all() 改造无明显优点冗余,浪费并行优势无推荐场景

关键注意事项

  1. 避免用 for...in/forEach 配合 awaitforEach 是同步迭代,await 不会阻塞循环,会导致所有任务并行执行(而非顺序)。
// 错误示例:forEach 中 await 无效,任务并行执行
async function wrongRun() {
  const results = [];
  [0,1,2,...,9].forEach(async (i) => {
    const result = await asyncTask(i);
    results.push(result);
  });
  console.log(results); // 空数组(循环已结束,任务未完成)
}
  1. 错误处理:顺序执行时,建议给每个任务单独加错误捕获(try/catch 或 then().catch()),避免一个任务失败导致整个流程中断(除非业务需要中断)。

  2. 性能优化:如果任务之间无依赖(仅需顺序执行,无需前一个结果),且想提升性能,可考虑 “有限并行”(如同时执行 2 个任务,完成后补位),但需用额外逻辑控制(如 p-limit 库)。

总结

  • 现代项目首选 async/await + for 循环:简洁、直观、易维护。
  • 旧环境选 Promise 链式调用:兼容 ES6,无需额外依赖。
  • 动态任务列表选递归调用:灵活处理不确定数量的任务。
  • 坚决避免 forEach + await 和 Promise.all() 强行顺序执行的写法。