ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Polar 前端性能实践:数组比较先做长度检查(Early Length Check)的正确姿势

Polar 前端性能实践:数组比较先做长度检查(Early Length Check)的正确姿势 Polar 前端性能实践数组比较先做长度检查Early Length Check的正确姿势【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar本文基于 Polar 仓库内.agents/skills/vercel-react-best-practices技能包中的js-length-check-first规则系统讲解在 JavaScript/TypeScript 中比较两个数组时为何应当先进行 O(1) 的长度检查再执行昂贵比较并结合 Polar 前端React Next.js的真实源码与测试展示该优化在事件处理、表单变更检测、渲染循环等热路径上的落地方式。读完本文你将掌握一套可复制的长度优先数组比较写法以及与之配套的不可变排序与提前返回优化。规则出处与本仓库中的定位该规则位于仓库的 js-length-check-first.md属于vercel-react-best-practices技能包SKILL.md中JavaScript Performancejs-前缀类别的一条规则标定影响等级为MEDIUM-HIGH其影响描述为当长度不同时避免昂贵操作avoids expensive operations when lengths differ。在该技能包的优先级排序中js-类规则位于第七档LOW-MEDIUM但其中不少规则包括本规则在热路径代码中的实际收益非常可观。SKILL.md 中对该规则的速查描述为js-length-check-first- Check array length before expensive comparison这条规则与同一类别下的js-early-exit提前返回、js-tosorted-immutable用toSorted()保证不可变排序天然互补经常成组出现在同一次代码审查或重构中。问题本质昂贵的比较操作不该无条件执行在真实业务代码中判断两个数组是否相等是极常见的需求例如用户编辑了某个集合已启用的功能 ID、商品价格 ID、多选标签后需要判断内容是否发生变化从而决定是否弹未保存更改提示或是否触发提交。最常见的偷懒写法是先排序、再拼接成字符串、再比较字符串// ❌ 错误无论长度是否相等都执行昂贵的比较 function hasChanges(current: string[], original: string[]) { // 长度不同时也会执行排序和 join return current.sort().join() ! original.sort().join() }这段代码的问题在于current.length为 5、original.length为 100 时两轮sort()各为 O(n log n)依然会完整执行随后还有拼接字符串、比较字符串的额外开销——而这一切本可以避免因为长度不同的数组必然不相等。具体开销可拆解为两次sort()排序时间复杂度各为 O(n log n)两次join()拼接需要分配与数组元素总量成正比的字符串内存大数组下不可忽视一次字符串全量比较。正确实现O(1) 长度短路 有序逐元素比较规则给出的正确写法如下// ✅ 正确先做 O(1) 长度检查再在长度相等时逐元素比较 function hasChanges(current: string[], original: string[]) { // 长度不同直接提前返回 if (current.length ! original.length) { return true } // 仅在长度相同时才排序 const currentSorted current.toSorted() const originalSorted original.toSorted() for (let i 0; i currentSorted.length; i) { if (currentSorted[i] ! originalSorted[i]) { return true } } return false }该实现的关键点与收益长度检查是 O(1) 的短路length是数组的内建属性读取成本恒定。长度不等直接返回true两个数组无论多大之间的排序、拼接、比较全部跳过只在必要时排序长度相等才进入排序分支且元素比较采用逐项短路发现第一个不同就返回无需等到全部比完零字符串内存开销不再构造join()产生的中间字符串对包含大量元素如上千个 ID的数组尤为重要不修改原数组toSorted()返回新数组详见下文与js-tosorted-immutable规则的呼应避免对传入的 props/state 造成意外变更提前返回循环中命中差异立即return true体现js-early-exit规则的精神。从数组相等到集合是否变更本仓库中的实战印证Polar 前端代码库clients/目录React Next.js中就有多处长度优先的数组比较实践可作为该规则的最佳注脚。印证一i18n 工具库的通用arraysEqualclients/packages/i18n/scripts/utils.ts 中定义了一个通用的数组相等判断/** * Check if two arrays are equal (same elements in same order) */ export function arraysEqual(a: string[], b: string[]): boolean { if (a.length ! b.length) return false return a.every((val, i) val b[i]) }注意这里的判断是相同顺序下的相等与规则的无序相等场景略有不同但其骨架完全一致先比较长度长度不等直接return false长度相等才用every逐项比较。every本身也是短路求值——遇到第一个不相等元素即停止。这是规则长度检查优先最直接、最干净的通用形态。印证二商品编辑页的权益是否变更检测clients/apps/web/src/components/Products/EditProductPage.tsx 在表单场景中判断启用的权益 ID 列表是否发生变化const hasBenefitsChanged useMemo( () enabledBenefitIds.length ! originalBenefitIds.length || enabledBenefitIds.some((id, index) id ! originalBenefitIds[index]), [enabledBenefitIds, originalBenefitIds], )这里hasBenefitsChanged被用于useEffect中驱动未保存更改提醒第113-115行useEffect(() { alertOnUnsavedChanges(formState.isDirty || hasBenefitsChanged) }, [formState.isDirty, hasBenefitsChanged, alertOnUnsavedChanges])由于enabledBenefitIds与originalBenefitIds都是按索引对应比较长度不等必然意味着发生了增删因此length !的判断被放在||的最左侧作为第一道短路。这段代码运行在每次表单状态变化触发的渲染路径上useMemo依赖变更即重算正是规则所说hot paths热路径的典型——提前用 O(1) 判断挡掉绝大多数无需逐项比较的情况。印证三商品选择器的新价格判断clients/apps/web/src/components/Products/ProductCombobox.tsx 中的hasNewPricing采用了同样的模式const hasNewPricing ( product: schemas[Product], currentPriceIds: string[], ) { const productPriceIds product.prices.map(({ id }) id) if (productPriceIds.length ! currentPriceIds.length) return true return !productPriceIds.every((id) currentPriceIds.includes(id)) }同样地先把数量不等这一最廉价、最强信号的判断放在最前面数量相等才进入every的逐项包含性检查。这再次印证长度检查是数组比较中最划算的第一步判断因为它以恒定成本直接区分了必然不等与可能相等两个分支。与配套规则的协同toSorted()与不可变比较正确示例中使用的current.toSorted()/original.toSorted()本身即另一条规则js-tosorted-immutable的核心主张详见 js-tosorted-immutable.md.sort()会原地修改数组。在 React 中对 props 或 state 数组调用.sort()属于破坏不可变模型的变更操作可能引发陈旧闭包、意外副作用等 bug.toSorted()返回新数组、原数组不变适合在useMemo、事件回调等场景中安全使用浏览器兼容基线为 Chrome 110、Safari 16、Firefox 115、Node.js 20对更老的运行环境可用展开运算符兜底[...items].sort(...)。Polar 代码库同样大量采用toSorted()例如 clients/apps/web/src/utils/bulk.test.ts 中测试代码用[...].toSorted()构造排序后的期望值并在第 72 行用expect(result.cancelled.toSorted()).toEqual([2, 3, 4])进行断言。这些测试用例同时验证了并发批量执行runBulk在成功、失败、取消三种路径下的结果集合排序仅为断言服务绝不允许污染原始结果数组——这正是不可变排序在真实工程中的价值体现。何时适用、何时慎用边界与注意事项无序相等集合比较若比较的是包含相同元素即可的集合需先排序再逐元素比较本文示例正是这种形态有序相等序列比较若顺序本身有意义如 i18n 工具中的arraysEqual则完全不需要排序直接length检查 逐项比较即可成本更低不要过早优化长度检查本身没有坏处但对只有两三个元素的微型数组其收益可忽略真正值得落地的场景是比较发生在渲染、事件回调、表单校验等高频热路径且数组规模可能较大的场景结合js-early-exit长度检查本质上是函数入口处最早的提前返回编写时请保持函数单一职责让短路分支一目了然便于审查者确认长度不等 ⇒ 必然不同这一不变式成立注意引用与变更语义长度检查只能说明元素数量相同不能说明元素内容相同因此长度相等后的逐项比较不可省略。小结js-length-check-first规则给出了一条简单而高频有效的优化原则在比较两个数组之前先用 O(1) 的length判断拦截必然不等的分支。它可以避免长度不等时仍执行 O(n log n) 排序与字符串拼接避免大数组下为拼接字符串分配多余内存配合toSorted()避免原数组被原地修改配合短路逐项比较在发现第一个差异时立即返回。Polar 仓库中 utils.ts、EditProductPage.tsx、ProductCombobox.tsx 三处实现分别覆盖了通用工具函数表单变更检测选择器状态判断三类真实场景可以作为团队内落地该规则的参考范例更完整的规则集合可查阅 SKILL.md 与 AGENTS.md。【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表