首页
/ Cal.diy 性能规则解析:合并多次数组遍历,将 N 次迭代压缩为 1 次

Cal.diy 性能规则解析:合并多次数组遍历,将 N 次迭代压缩为 1 次

2026-09-05 18:14:47作者:段琳惟

本文基于 Cal.diy(Cal.com 开源版)仓库内置的 Vercel React 最佳实践技能库,深入讲解 js-combine-iterations 规则:为什么对同一数组发起多次 .filter() / .map() 是应被识别和重构的性能反模式,如何将其合并为单次循环,以及在当前代码库中这种模式出现在哪些真实场景里。读完本文,你将掌握该规则的完整判定标准、改写范式,并能结合仓库源码验证其实际应用与边界。

一、规则定位:45 条性能规则中的 JavaScript 分类

该规则定义在 js-combine-iterations.md,其 YAML 元数据声明了规则的完整画像:

元数据字段 取值 含义
title Combine Multiple Array Iterations 合并多次数组遍历
impact LOW-MEDIUM 影响等级为中低(属渐进式优化,非关键路径瓶颈)
impactDescription reduces iterations 核心收益:减少迭代次数
tags javascript, arrays, loops, performance 标签覆盖 JS、数组、循环、性能四个维度

这个技能库的总览文件 SKILL.md 将 45 条规则分为 8 个按影响优先级排列的类别。js-combine-iterations 位于第 7 类「JavaScript Performance(LOW-MEDIUM)」,与 js-index-maps(为重复查找构建 Map)、js-set-map-lookups(Set/Map 的 O(1) 查找)、js-early-exit(函数提前返回)、js-min-max-loop(用循环代替排序求极值)等规则并列。这意味着它和消除瀑布式等待(async- 类,CRITICAL)、包体积优化(bundle- 类,CRITICAL)不在同一量级,而是在热点循环内部做常数级迭代次数削减的精细优化。

规则的完整展开版本见技能库的编译文档 AGENTS.md(第 7.6 节),内容与独立规则文件完全一致,可作为 LLM/Agent 执行自动重构时的引用入口。

二、规则核心:反模式识别与改写范式

规则原文给出的判定标准是一句话:

Multiple .filter() or .map() calls iterate the array multiple times. Combine into one loop. (多次 .filter().map() 调用会使数组被遍历多次。将它们合并进一个循环。)

2.1 反模式:3 次遍历

const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)

三段代码看似各不相干,但每次调用都完整扫描一遍 users:3 次数组遍历、3 次迭代器创建、3 个中间数组的分配。若 users 长度为 n,总工作量是 O(3n)。

2.2 正确写法:1 次遍历

const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []

for (const user of users) {
  if (user.isAdmin) admins.push(user)
  if (user.isTester) testers.push(user)
  if (!user.isActive) inactive.push(user)
}

改写要点有三:

  1. 先声明结果数组(带显式类型标注 User[]),在循环外一次性分配;
  2. 单循环内并行判断:每个元素只做一次属性读取和一次循环推进,多个条件互不短路、互不影响,因此 adminstestersinactive 三个集合与多次 filter 的结果逐元素等价
  3. push 摊还 O(1),总复杂度从 O(kn)(k 为遍历次数)降为 O(n),且只创建 1 个迭代器。

这里有一个容易被忽视的等价性细节:原例中 isAdminisTester!isActive相互独立的谓词(一个用户可能既是 admin 又是 tester),所以合并为多个 if 是正确的;如果各谓词互斥(命中其一即应停止),则应改用 if / else if 链,语义与性能会不同。改写时必须先确认谓词之间是否互斥,这是应用该规则时的首要检查点。

三、仓库实证:Cal.diy 代码库中的真实案例

该规则并非纸面理论,当前仓库源码中存在符合该模式的真实代码。RegularBookingService.ts 中处理团队事件的成员筛选逻辑:

const teamDestinationCalendars: DestinationCalendar[] = [];
const fixedUsers = users.filter((user) => user.isFixed);
const nonFixedUsers = users.filter((user) => !user.isFixed);
const filteredUsers =
  schedulingType === SchedulingType.ROUND_ROBIN ? [...fixedUsers, ...nonFixedUsers] : users;

这里 users 被连续两次 .filter() 扫描,且两个谓词(user.isFixed / !user.isFixed互斥——这正是 2.2 节强调需要区分的场景。按该规则的范式,这段代码可以改写为单循环分桶:

const fixedUsers: typeof users = [];
const nonFixedUsers: typeof users = [];

for (const user of users) {
  if (user.isFixed) {
    fixedUsers.push(user)
  } else {
    nonFixedUsers.push(user)
  }
}

值得注意的是,此处保留双 filter 写法有一定现实合理性:团队事件的用户列表规模有限(一个团队成员通常几十人量级),而 ROUND_ROBIN 模式下「固定用户排前、非固定用户排后」的拼接意图([...fixedUsers, ...nonFixedUsers])在双 filter 版本中更直白。这也体现了该规则 impact: LOW-MEDIUM 的定位——它是应被知晓的优化项,但不是无条件强制项,在数据规模小、可读性收益明显时保留原写法是合理权衡。

此外,data-table GUIDE.md 的示例代码中也出现了 usersactiveUsersinactiveUsers 两次 filter 的同类结构,进一步说明「同一数组、互补谓词、双 filter 分桶」是代码库中反复出现的惯用结构,值得作为该规则的标准识别模式。

四、纵深关联:与 O(n²) 规避规则的关系

Cal.diy 的 Agent 规则库中另有一条 CRITICAL 级规则 performance-avoid-quadratic.md,明确把「链式 filter(Chained filters)和嵌套映射」列为 O(n²) 反模式的来源之一:

  • Chained filters or nested mapping over large lists
  • Array methods like .some, .find, or .filter inside loops or callbacks

两条规则构成互补关系:js-combine-iterations 处理的是同一数组上的横向冗余遍历(k 次完整扫描 → 1 次),属于常数因子优化;而 performance-avoid-quadratic 处理的是谓词内部又对另一数组做线性扫描.filter 回调里嵌套 .some),属于复杂度量级优化。前者将 O(kn) 压到 O(n),后者将 O(n·m) 压到 O((n+m) log(n+m))。在代码审查中同时应用两者时,优先级应以后者为先——量级下降永远优于常数优化,这与技能库把 js- 类规则排在 CRITICAL/HIGH 规则之后的分类逻辑一致。

五、适用边界与判定清单

结合规则原文与仓库实践,将该规则的适用条件整理为一份可操作清单:

  • 适用:同一数组在相同数据状态下被 2 次及以上遍历;各谓词相互独立且只做读取(无副作用、无中间状态依赖);数据规模足以让多次扫描产生可测量开销(热路径、大数组、服务端批量处理)。
  • 需谨慎:谓词互斥时改用 if / else if(见 RegularBookingService 案例);谓词之间存在依赖(第二个 filter 的输入是第一个 filter 的输出)时不能合并,那是流水线而非冗余遍历;数组规模极小且原代码意图自明时,可读性可能优先于常数优化。
  • 不适用:遍历次数本身很少(单次 filter/map);不同数组之间的多次遍历(合并无从谈起);异步谓词场景(单循环内 await 会串行化,应优先考虑 Promise.all 等并发手段,对应技能库 async-parallel 规则)。

六、小结

js-combine-iterations 是 Vercel React 最佳实践技能库中 JavaScript 性能分类下的基础规则,核心主张简单而明确:对同一数组的多次 .filter() / .map() 应合并为单次循环,用循环内分支判断替代多次完整扫描。在 Cal.diy 这样的企业级调度基础设施代码库中,团队事件成员筛选等场景存在真实的多次遍历结构;将其与 CRITICAL 级的 O(n²) 规避规则区分开——一个管常数、一个管量级——可以在代码审查与 AI 辅助重构中做出正确的优先级判断。规则文件、完整技能目录与上文引用的源码路径均保留在当前仓库中,可供进一步查证。

登录后查看全文
热门项目推荐
相关项目推荐