SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

forEach 中使用 async/await 的异步陷阱

前言:数据不是最新的?

在处理数组异步操作时,很容易踩到一个常见的坑:在 forEach 中使用 async 函数,导致后续代码无法获取到最新数据。

典型症状:

  • ✅ 代码逻辑看起来没问题
  • ✅ 异步操作也执行了
  • ❌ 但后续读取数据时,获取的还是旧数据

一、问题复现

1.1 错误代码

const fetchConversationList = async (): Promise<boolean> => {
  try {
    const response = await getCustomerList();

    if (response.code === 200) {
      // ❌ 问题:forEach 不会等待异步操作完成
      response.data.forEach(async (item) => {
        const { customerName, id, imageUrl, isNew, labelList, userNotifications } = item;

        if (isNew) {
          const dataDB = {
            sessionId: id,
            customerInfo: { name: customerName, avatar: imageUrl },
            messages: [...],
            // ...其他数据
          };

          // 异步保存到数据库
          await messageStorage.saveConversationDB(id, dataDB);
        }
      });

      // ❌ 问题:这里执行时,上面的数据可能还没保存完成
      await loadConversationListFromDB(activeAccount.value);
      return true;
    }
  } catch (err) {
    console.error(err);
    return false;
  }
};

1.2 问题分析

为什么会出现这个问题?

  1. forEach 不支持异步等待

    • forEach 只是一个普通的循环方法
    • 即使回调函数是 async,forEach 也不会等待它完成
    • forEach 会立即返回,不管回调函数中的 await 是否完成
  2. 执行流程

    开始 forEach
    ├─ 启动第1个异步任务 (saveConversationDB)
    ├─ 启动第2个异步任务 (saveConversationDB)
    ├─ 启动第3个异步任务 (saveConversationDB)
    └─ forEach 立即结束 ⚡️
    
    执行 loadConversationListFromDB ❌ (数据库操作还在进行中)
    
    第1个任务完成 ✓
    第2个任务完成 ✓
    第3个任务完成 ✓
  3. 结果

    • loadConversationListFromDB 执行时,数据库写入可能还没完成
    • 读取到的是上一次的旧数据
    • 用户看到的列表不是最新的

二、底层原理深度解析

2.1 forEach 的实现原理

让我们看看 forEach 的内部实现(简化版):

Array.prototype.forEach = function (callback) {
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this); // 只是调用回调函数,不关心返回值
  }
  // forEach 本身没有返回值 (返回 undefined)
};

关键点:

  1. forEach 只是简单地调用回调函数
  2. 不关心回调函数的返回值(即使返回 Promise 也不处理)
  3. 回调函数立即执行完就继续下一轮循环

当使用 async 回调时发生了什么?

// 你的代码
[1, 2, 3].forEach(async (item) => {
  await doSomething(item);
});

// 实际执行等价于
for (let i = 0; i < arr.length; i++) {
  // async 函数会立即返回一个 Promise
  const promise = (async (item) => {
    await doSomething(item);
  })(arr[i]);

  // ❌ forEach 不会 await 这个 Promise,直接丢弃!
  // promise 继续在后台运行,但 forEach 已经进入下一轮循环了
}

结论:forEach 并不是变成异步了,而是它忽略了异步操作!


2.2 for...of 为什么可以?

for...of 是 ES6 新增的语法结构(不是方法),支持 await。

// for...of 的执行方式
for (const item of [1, 2, 3]) {
  await doSomething(item); // ✅ 会真正等待
}

// 等价于
const item1 = arr[0];
await doSomething(item1); // 等待完成

const item2 = arr[1];
await doSomething(item2); // 等待完成

const item3 = arr[2];
await doSomething(item3); // 等待完成

为什么可以等待?

  1. for...of 是语言级别的控制流语句

    • 类似 if、while,不是函数调用
    • JavaScript 引擎直接支持 await 暂停执行
  2. 每次迭代都会等待当前 await 完成

    • 遇到 await 时,暂停当前迭代
    • 等待 Promise 完成后,才进入下一次迭代

对比:

特性forEachfor...of
类型数组方法(函数调用)语言语法结构
await 支持❌ 不支持(会被忽略)✅ 原生支持
执行方式同步调用所有回调逐步迭代,可暂停
返回值undefined无返回值(语句)

2.3 Promise.all 为什么可以?

Promise.all 的原理完全不同,它是通过收集 Promise 并等待的方式工作。

原理拆解:

// 你的代码
await Promise.all(
  [1, 2, 3].map(async (item) => {
    await doSomething(item);
  })
);

// 等价于
// 第一步:map 收集所有 Promise
const promises = [1, 2, 3].map(async (item) => {
  return await doSomething(item);
});

// promises = [Promise1, Promise2, Promise3]

// 第二步:Promise.all 等待所有 Promise 完成
await Promise.all(promises);

执行流程:

时间线:
0ms  → map 立即执行,创建 3 个 Promise (Promise1, Promise2, Promise3)
0ms  → 3 个异步任务同时开始执行 ⚡️⚡️⚡️
100ms → Promise1 完成 ✓
150ms → Promise2 完成 ✓
200ms → Promise3 完成 ✓
200ms → Promise.all 等待结束,继续执行后续代码

Promise.all 简化实现:

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;

    promises.forEach((promise, index) => {
      // ✅ 关键:通过 .then() 等待每个 Promise
      promise
        .then((result) => {
          results[index] = result;
          completed++;

          // 所有 Promise 都完成了
          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
};

为什么可以等待?

  1. map 返回了 Promise 数组

    • async 函数会返回 Promise
    • map 收集这些 Promise(而 forEach 忽略返回值)
  2. Promise.all 主动等待所有 Promise

    • 通过 .then() 监听每个 Promise 的完成状态
    • 只有全部完成,才 resolve

对比:

特性forEach + asyncmap + Promise.all
返回值undefinedPromise 数组
是否等待❌ 不等待(丢弃 Promise)✅ 收集并等待所有 Promise
执行方式异步启动,不等待并行执行,等待全部完成

2.4 三种方案的内部对比

场景:处理 3 个异步任务,每个任务耗时 100ms

方案 1:forEach(错误)

console.log("开始");
[1, 2, 3].forEach(async (n) => {
  await sleep(100);
  console.log(`任务${n}完成`);
});
console.log("结束");

// 输出:
// 开始
// 结束          ← forEach 立即返回
// (100ms 后)
// 任务1完成      ← 异步任务在后台完成
// 任务2完成
// 任务3完成

总耗时: 0ms(forEach 立即返回,不等待)


方案 2:for...of(串行)

console.log("开始");
for (const n of [1, 2, 3]) {
  await sleep(100);
  console.log(`任务${n}完成`);
}
console.log("结束");

// 输出:
// 开始
// (100ms 后)
// 任务1完成
// (100ms 后)
// 任务2完成
// (100ms 后)
// 任务3完成
// 结束          ← 所有任务完成后才继续

总耗时: 300ms(串行执行:100 + 100 + 100)


方案 3:Promise.all(并行)

console.log("开始");
await Promise.all(
  [1, 2, 3].map(async (n) => {
    await sleep(100);
    console.log(`任务${n}完成`);
  })
);
console.log("结束");

// 输出:
// 开始
// (100ms 后,几乎同时)
// 任务1完成
// 任务2完成
// 任务3完成
// 结束          ← 所有任务完成后才继续

总耗时: 100ms(并行执行:max(100, 100, 100))


2.5 核心原理总结

方法是否异步?是否等待?原理
forEach回调是异步的❌ 不等待只调用回调,忽略返回的 Promise
for...of支持异步✅ 等待语法级支持,遇到 await 会暂停迭代
Promise.all并行异步✅ 等待收集 Promise 数组,监听所有 Promise 完成

关键理解:

  1. forEach 没有变成异步

    • forEach 本身是同步的
    • 它只是启动了异步任务,但不等待结果
    • 就像「点了 3 份外卖,但不等外卖送到就离开了」
  2. for...of 是语法级支持

    • 不是方法调用,是语言特性
    • JavaScript 引擎知道如何处理 await
    • 就像「做完一件事,再做下一件事」
  3. Promise.all 是主动等待

    • 收集所有 Promise
    • 监听它们的完成状态
    • 就像「同时点 3 份外卖,等全部送到再吃」

三、解决方案

3.1 方案一:使用 for...of(推荐 - 串行执行)

适用场景: 需要按顺序执行,或者操作之间有依赖关系

const fetchConversationList = async (): Promise<boolean> => {
  try {
    const response = await getCustomerList();

    if (response.code === 200) {
      // ✅ 使用 for...of 循环
      for (const item of response.data) {
        const { customerName, id, imageUrl, isNew, labelList, userNotifications } = item;

        if (isNew) {
          const dataDB = {
            sessionId: id,
            customerInfo: { name: customerName, avatar: imageUrl },
            messages: [...],
          };

          // await 会等待每个操作完成
          await messageStorage.saveConversationDB(id, dataDB);
        }
      }

      // ✅ 所有数据都已保存完成,可以安全读取
      await loadConversationListFromDB(activeAccount.value);
      return true;
    }
  } catch (err) {
    console.error(err);
    return false;
  }
};

执行流程:

第1个任务开始 → 等待完成 ✓
第2个任务开始 → 等待完成 ✓
第3个任务开始 → 等待完成 ✓
执行 loadConversationListFromDB ✓ (所有数据已写入)

优点:

  • 代码简单易懂
  • 执行顺序可预测
  • 适合有依赖关系的操作

缺点:

  • 串行执行,速度较慢
  • 如果有 100 条数据,需要等待 100 次操作依次完成

3.2 方案二:使用 Promise.all(推荐 - 并行执行)

适用场景: 操作之间没有依赖,可以并行执行以提高性能

const fetchConversationList = async (): Promise<boolean> => {
  try {
    const response = await getCustomerList();

    if (response.code === 200) {
      // ✅ 使用 Promise.all 并行执行所有异步操作
      await Promise.all(
        response.data.map(async (item) => {
          const { customerName, id, imageUrl, isNew, labelList, userNotifications } = item;

          if (isNew) {
            const dataDB = {
              sessionId: id,
              customerInfo: { name: customerName, avatar: imageUrl },
              messages: [...],
            };

            await messageStorage.saveConversationDB(id, dataDB);
          }
        })
      );

      // ✅ 所有并行任务都已完成,可以安全读取
      await loadConversationListFromDB(activeAccount.value);
      return true;
    }
  } catch (err) {
    console.error(err);
    return false;
  }
};

执行流程:

同时启动所有任务
├─ 第1个任务 ⚡️
├─ 第2个任务 ⚡️
├─ 第3个任务 ⚡️
└─ 等待所有任务完成 ✓

执行 loadConversationListFromDB ✓ (所有数据已写入)

优点:

  • 并行执行,速度最快
  • 100 条数据几乎同时处理

缺点:

  • 如果某个操作失败,整个 Promise.all 会失败
  • 并发太高可能造成性能问题

3.3 方案三:使用 Promise.allSettled(容错处理)

适用场景: 需要确保所有操作都执行完,即使部分失败

const fetchConversationList = async (): Promise<boolean> => {
  try {
    const response = await getCustomerList();

    if (response.code === 200) {
      // ✅ 使用 Promise.allSettled 等待所有任务(包括失败的)
      const results = await Promise.allSettled(
        response.data.map(async (item) => {
          const { customerName, id, imageUrl, isNew } = item;

          if (isNew) {
            const dataDB = {
              /* ... */
            };
            await messageStorage.saveConversationDB(id, dataDB);
          }
        })
      );

      // 检查失败的任务
      const failedTasks = results.filter((r) => r.status === "rejected");
      if (failedTasks.length > 0) {
        console.warn(`${failedTasks.length} 个会话保存失败`);
      }

      await loadConversationListFromDB(activeAccount.value);
      return true;
    }
  } catch (err) {
    console.error(err);
    return false;
  }
};

优点:

  • 部分失败不影响整体流程
  • 可以收集所有失败的任务进行处理

四、常见数组遍历方法对比

方法支持 async/await执行方式适用场景
forEach❌ 不支持立即返回,不等待同步操作,无需等待结果
for...of✅ 支持串行执行,按顺序等待需要顺序执行的异步操作
Promise.all + map✅ 支持并行执行,等待所有完成可并行的异步操作(性能最优)
Promise.allSettled + map✅ 支持并行执行,容错处理需要容错的并行异步操作
for / while✅ 支持串行执行复杂控制流程

五、实际案例对比

5.1 错误示例:forEach

// ❌ 错误:forEach 不等待异步操作
response.data.forEach(async (item) => {
  await saveToDatabase(item); // 不会等待
});
console.log("完成"); // 立即执行,数据可能还没保存

5.2 正确示例:for...of

// ✅ 正确:for...of 会等待每个操作完成
for (const item of response.data) {
  await saveToDatabase(item); // 等待完成
}
console.log("完成"); // 所有数据已保存

5.3 性能优化:Promise.all

// ✅ 最优:并行执行,速度最快
await Promise.all(
  response.data.map(async (item) => {
    await saveToDatabase(item);
  })
);
console.log("完成"); // 所有数据已保存

六、最佳实践建议

6.1 选择合适的方法

  1. 需要等待异步操作?

    • ❌ 不要用 forEach
    • ✅ 用 for...of 或 Promise.all
  2. 操作之间有依赖?

    • ✅ 用 for...of(串行)
    • ❌ 不要用 Promise.all(并行)
  3. 追求性能?

    • ✅ 用 Promise.all(并行)
    • ⚠️ 注意并发控制
  4. 需要容错?

    • ✅ 用 Promise.allSettled
    • 可以继续处理成功的任务

6.2 代码检查清单

在使用数组遍历异步操作时,检查:

  • [ ] 是否在 forEach 中使用了 async/await?
  • [ ] 后续代码是否依赖异步操作的结果?
  • [ ] 是否需要等待所有操作完成?
  • [ ] 操作之间是否有依赖关系?
  • [ ] 是否需要并行执行以提高性能?

七、总结

7.1 核心要点

  1. forEach + async/await = 陷阱

    • forEach 不会等待异步操作
    • 会导致后续代码读取到旧数据
  2. 正确的做法

    • 串行执行:使用 for...of
    • 并行执行:使用 Promise.all
    • 容错处理:使用 Promise.allSettled
  3. 记住这个规则

    如果数组遍历中有 await,永远不要用 forEach

7.2 快速决策树

需要等待异步操作完成吗?
├─ 否 → 使用 forEach
└─ 是
    ├─ 需要串行执行吗?
    │   ├─ 是 → 使用 for...of
    │   └─ 否
    │       ├─ 需要容错吗?
    │       │   ├─ 是 → 使用 Promise.allSettled
    │       │   └─ 否 → 使用 Promise.all
    │       └─
    └─

参考资源

  • MDN - Array.prototype.forEach()
  • MDN - for...of
  • MDN - Promise.all()
  • MDN - Promise.allSettled()
最后更新时间:
贡献者: 何风顺
上一页
reduce