首页
/ Angular NG0955 错误深入解析:`@for` 循环 track 表达式生成重复键的成因、危害与修复

Angular NG0955 错误深入解析:`@for` 循环 track 表达式生成重复键的成因、危害与修复

2026-09-07 11:49:54作者:彭桢灵Jeremy

NG0955 是 Angular 内置控制流语法 @for 在列表协调(list reconciliation)阶段于开发模式(dev mode)下报告的一组运行时错误信息,其场景是:某条 track 表达式对同一集合中的多个不同元素计算出了相同的追踪键。本文以 Angular 官方错误文档 adev/src/content/reference/errors/NG0955.md 为骨架,结合 list_reconciliation.ts 等真实源码与对应测试,说明该错误的触发方式、本质成因、正确性/性能双重危害,以及如何改写 track 表达式从根本上消除它,帮助你在开发 @for 动态列表时写出正确且高效的 diff 逻辑。

一、错误概述:什么时候会看到 NG0955

@for 循环中指定的 track 表达式对某个给定集合计算出了重复的键时,Angular 会在开发模式下于控制台输出形如下面的警告信息:

NG0955: The provided track expression resulted in duplicated keys for a given collection. Adjust the tracking expression such that it uniquely identifies all the items in the collection. Duplicated keys were:
key "a" at index "0" and "2".

这段输出来自 list_reconciliation.ts:协调结束后,Angular 把重复键连同它们出现的索引拼接成 Duplicated keys were: ... 的消息体,经由 errors.ts 中定义的 RuntimeErrorCode.LOOP_TRACK_DUPLICATE_KEYS = -955(对应错误号 NG0955)与 formatRuntimeError 格式化为带 NG0955 前缀的标准文案后,通过 console.warn 输出。也就是说,NG0955 不是崩溃性的抛错,而是开发模式下控制台中的告警,用于提前暴露列表追踪键的设计缺陷。

二、最小复现示例

官方错误文档给出了一个十分典型的复现场景:track 使用了无法唯一标识元素的字段。例如按 item.value 追踪,而集合里恰好有两个元素的 value 相同:

@Component({
  template: `@for (item of items; track item.value) {
    {{ item.value }}
  }`,
})
class Test {
  items = [
    {key: 1, value: 'a'},
    {key: 2, value: 'b'},
    {key: 3, value: 'a'},
  ];
}

在以上示例中,track item.value 会分别在索引 0 与索引 2 上计算出两个重复键 a——键 a 同时映射到两个不同的集合元素,破坏了“一键一元素”的唯一性前提。

从源码实现看,重复键的探测确实发生在 Angular 的 @for 更新机制内部。每次集合变更触发更新时,渲染器都会调用 reconcile() 对“现存 live 集合”与“新到来的集合”进行就地协调;该函数在 ngDevMode 下为每次读取键值维护一张 Map<unknown, Set<number>> 的索引表(见 recordDuplicateKeys),并沿着从头尾两端向中间扫描的路径,把新集合中每个索引计算出的键登记进去(见 list_reconciliation.tslist_reconciliation.ts)。只要同一个键对应的索引集合大小大于 1,即判定为重复。

三、为什么重复键是错误的

1. 正确性层面:DOM 节点可能与错误数据绑定

@for 的 diff 逻辑依赖追踪键来唯一地辨认“旧集合里的哪个元素对应新集合里的哪个元素”。一旦键重复,Angular 便无法在两次渲染之间可靠地对应条目,执行移动(move)或销毁(destroy)操作时,很可能把本属于另一个元素(拥有相同键的那个元素)的 DOM 节点当作目标,导致视图复用错位、展示的数据与模板解绑错误。文档原话即是:since the @for loop can't uniquely identify items it might choose DOM nodes corresponding to _another_ item (with the same key) when performing DOM moves or destroy

2. 性能层面:被迫退入更慢的数据结构

重复键还会带来额外的性能惩罚。当键唯一时,协调算法可以大部分时间停留在“快速路径”(fast path)上,几乎不需要额外分配内存;而一旦键出现重复,算法就不得不初始化更复杂的辅助结构来记录哪些键曾被分离、哪些键在未来还会出现。

这一点在 list_reconciliation.ts 的注释中说得非常直白:算法存在两条代码路径——“fast”路径不产生任何内存分配,而“slow”路径需要为中间数据结构分配额外内存;协调过程会在同一轮中尽量停留在 fast 路径,但当无法直接匹配时就会惰性创建 UniqueValueMultiKeyMap(见 list_reconciliation.ts)。该结构允许“一个键对应多个值”,并用独立的“值到下一值”链表维护 FIFO 顺序(见 UniqueValueMultiKeyMap 的实现),其注释也说明设计目标是在“键不重复”这一最常见场景下保持最小开销。换言之:重复键越多、出现越频繁,Angular 就越需要依赖更臃肿、更慢的结构来兜底,这正是错误文档所指出的 performance penalty。

四、修复方法:让 track 表达式唯一标识每一个条目

修复思路只有一条主线:改写追踪表达式,使它为集合中每个条目都计算出彼此不同的键。回到上文示例,每条数据都带有唯一的 key 属性,因此把追踪字段从 item.value 换成 item.key 即可:

@Component({
  template: `@for (item of items; track item.key) {
    {{ item.value }}
  }`,
})
class Test {
  items = [
    {key: 1, value: 'a'},
    {key: 2, value: 'b'},
    {key: 3, value: 'a'},
  ];
}

修改之后,三个条目分别得到键 123,全部唯一,@for 便能在 DOM 移动、增删时精确、无误地对上“人”。

结合上面复现示例,可以总结出挑选 track 键时的几条实操准则:

  • 优先选择数据中语义唯一的字段(数据库主键、id、uuid 等)作为追踪键;
  • 不要用可能重复的可展示字段(如 valuelabeltitle)追踪,即使它们在界面上允许重复;
  • 若数据本身缺少天然唯一字段,可用模板变量与索引组合的箭头函数形式,例如 track (item, index) => item.type + ':' + index,确保每个索引都能得到独立键;
  • 若列表永不重排、且元素身份固定,直接省略 track、退回到 Angular 基于引用/位置的默认追踪方式往往是安全的——只有当重复键与 DOM 复用同时出现时才需要显式提供唯一键。

五、深入源码:NG0955 的触发范围与边界

1. 仅在开发模式下生效

重复键的采集(duplicateKeys)与最终的报告都包裹在 ngDevMode 守卫内(见 list_reconciliation.tslist_reconciliation.ts)。因此 NG0955 属于开发期诊断手段,生产构建不会产生该告警开销,也不影响线上行为——但它指示的键设计缺陷在生产环境依然存在,只是不再显性提示。

2. 数组与通用可迭代对象走不同协调路径

源码区分了 Array.isArray(newCollection) 的分支(list_reconciliation.ts)与其余可迭代对象的惰性迭代分支(list_reconciliation.ts)。两条路径都会登记重复键,但可迭代对象(如 Set、生成器产物等)无法随机访问,一进入就基本退入 slow path,逐个调用迭代器取值后再与 live 集合比较。对于这两类数据源,测试 control_flow_for_spec.ts 都专门验证了“应当对重复键发出 NG0955 警告”的行为。

3. 键的类型与键值比较语义

追踪键不限于字符串或数字。测试用例表明 Symbol 类型的键同样会被纳入重复检测(见 control_flow_for_spec.ts);而键之间是否“相等”,在快速路径的匹配判断中统一使用 Object.is 语义比较(见 valuesMatching)。此外,即使键重复,@for 仍会尽力完成“正确分离再回挂”的操作——测试 control_flow_for_spec.ts 验证了在重复键场景下视图仍能正确 attach 与 detach,不会直接崩溃,这与“NG0955 以 console.warn 而非抛错形式报告”的实现相互印证。

4. 报错文案中的索引定位

为了方便你在真实集合中快速定位冲突,reconcile 末尾会逐个键按索引排序,把“同一个键出现的相邻两个索引”拼成 key "…" at index "x" and "y" 的条目后合并进告警正文(见 list_reconciliation.ts)。当你看到类似 key "a" at index "0" and "2" 时,直接到集合的第 0、2 项排查它们共用的追踪字段即可。

六、追踪键与 @for 的整体协调上下文

NG0955 只是 @for 列表更新管线中的一个诊断环节。要真正理解它的位置,可以从渲染器侧梳理整条调用链:

  • control_flow.ts 中定义 RepeaterContextRepeaterMetadata,其中保存了经过宿主组件上下文 bindtrackByFn(见 control_flow.ts);
  • 更新时,渲染器把“现存视图集合”抽象为 LiveCollection<T, V> 适配器(list_reconciliation.ts),统一封装了 attach/detach/create/destroy/move/swap 等操作——这些操作最终都对应真实 DOM/LView 的增删移换;
  • 随后调用 reconcile(liveCollection, collection, metadata.trackByFn, prevConsumer)(见 control_flow.ts)执行两端夹逼的键匹配、swap/move 检测、slow path 分离与最终清理;
  • NG0955 的重复键检查就发生在 reconcile 收尾阶段,即上面的“清理完成后统一报告”环节。

因此,当 @for 的界面出现“数据串行”“行被错误复用”之类的诡异现象,同时控制台伴有 NG0955 告警时,几乎可以断定问题出在追踪表达式而非数据本身——优先回头审查 track 键的唯一性,通常能一击命中。

七、小结

  • NG0955 在开发模式下报告 @for 的 track 表达式对同一集合产生重复键(RuntimeErrorCode.LOOP_TRACK_DUPLICATE_KEYS = -955,定义见 errors.ts);
  • 根因是 track 键未能唯一标识条目,后果是 DOM 移动/销毁时可能认错节点(正确性)以及协调被迫依赖 UniqueValueMultiKeyMap 等慢速结构(性能);
  • 修复方式是把 track 表达式改为使用集合内确实唯一的属性(如 item.key、id),或借助索引构造唯一键;
  • 该告警仅存在于 ngDevMode,生产构建不受影响,但键的唯一性问题本身需从设计上解决;
  • 相关行为均有测试覆盖,可参考 control_flow_for_spec.ts 理解数组、可迭代对象、Symbol 键等各类边界下的判定与输出。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388