Angular NG02200 Missing Iterable Differ 详解:*ngFor 为什么无法遍历字符串与对象
NG02200 是 Angular 在 *ngFor 绑定到非可迭代(non-iterable)值时抛出的运行时错误,官方名为 "Missing Iterable Differ"。本文以仓库中的错误文档 NG02200.md 为骨架,结合 NgForOf 指令、IterableDiffers 依赖注入机制及单元测试,解释该错误"为什么发生、底层如何抛出、如何修复",并给出迁移到新一代 @for 块(block)的推荐方案。读完你将能准确定位此类循环渲染报错,并写出既高效又类型安全的列表渲染代码。
错误长什么样
该错误的官方文档原文非常简短,只有两个要点:
NgFor无法为传入的值找到 iterable differ,请确保传入的是可迭代对象,例如Array;- 如果试图遍历对象的键(keys),应改用 KeyValuePipe 而不是
*ngFor。
在真实运行时,浏览器控制台里会看到类似下面的完整输出(消息末尾由框架自动拼接错误链接):
ERROR Error: NG02200: Cannot find a differ supporting object 'whaaa' of type 'string'.
NgFor only supports binding to Iterables, such as Arrays.
Find more at https://angular.dev/errors/NG02200
而当你传入的是普通对象时,提示会更"贴心",多出一句建议:
NG02200: Cannot find a differ supporting object '[object Object]' of type 'object'.
NgFor only supports binding to Iterables, such as Arrays.
Did you mean to use the keyvalue pipe?
注意错误码本身:NG02200 对应开发工具包中的负向运行时错误码 NG_FOR_MISSING_DIFFER = -2200,可参见 packages/common/src/errors.ts,对外展示时取绝对值并加 NG 前缀。
根因剖析:一条从模板到 Differ 的调用链
要真正理解这个错误,需要顺着 *ngFor 的展开与变更检测流程看下去。
1. NgForOf 指令在 ngDoCheck 中寻找 differ
模板里的 *ngFor="let item of items" 会被编译成 <ng-template ngFor [ngForOf]="items">,对应实现是 packages/common/src/directives/ng_for_of.ts 中的 NgForOf 指令。核心逻辑集中在 ngDoCheck():
- 首次拿到非空值时,调用
this._differs.find(value).create(...)创建该集合专用的 differ 实例(ng_for_of.ts); - 之后每次变更检测用该 differ 对集合做
diff(),把新增、删除、移动、身份变化映射为对 DOM 的插入、移除与重排操作。
其中最关键的一行是 this._differs.find(value)——如果没有任何 differ factory 支持传入值的类型,此处就会失败。
2. IterableDiffers:可迭代集合的"支持者登记处"
IterableDiffers 定义在 packages/core/src/change_detection/differs/iterable_differs.ts,它是一个"工厂仓库":
find(iterable: any): IterableDifferFactory {
const factory = this.factories.find((f) => f.supports(iterable));
if (factory != null) {
return factory;
} else {
throw new RuntimeError(...); // 没有任何 factory 支持该值
}
}
- 默认注册的工厂是
DefaultIterableDifferFactory,其supports()通过isListLikeIterable()判定传入值是否为类数组可迭代对象(见 packages/core/src/change_detection/differs/default_iterable_differ.ts); - 字符串、纯对象、数字等都不被支持,
find抛出底层错误; - 如需为自定义类型提供 diffing 策略,可通过
IterableDiffers.extend([...])注册新的IterableDifferFactory(用法注释见 iterable_differs.ts)。
3. 开发模式下"捕获→补全提示→重新抛出"
NgForOf 对 differ 创建过程做了值得注意的加工:生产构建直接调用 find().create();而开发构建(dev mode)则用 try-catch 包住这段逻辑(源码注释明确说明"this logic is duplicated",见 ng_for_of.ts),在 catch 中拼装面向开发者的诊断信息,并针对 typeof value === 'object' 的情况追加 "Did you mean to use the keyvalue pipe?" 提示,最终以 RuntimeError 形式抛出——这就是你看到的 NG02200。
这条逻辑也被单测精确锁定:packages/common/test/directives/ng_for_spec.ts 分别对字符串 'whaaa' 和对象 {'stuff': 'whaaa'} 断言了上述两条不同的报错文案。同一个测试文件中还有一组用例证明:当绑定的值变为 null 或 undefined 时 *ngFor 不会报错,而是安静地渲染空内容;从非空集合切换到 null 再切回来也都能优雅处理(ng_for_spec.ts)。这是因为 ngDoCheck 只在值存在(truthy)时才去创建 differ。
到底哪些值能过、哪些值会炸
按文档说法,*ngFor 必须收到"某种可迭代类型"。框架层面的类型别名也印证了这一点:
export type NgIterable<T> = Array<T> | Iterable<T>;
即 NgIterable = Array<T> 或实现了可迭代协议的 Iterable<T>(见 iterable_differs.ts)。实际常见情况归纳如下:
| 绑定值 | 结果 | 说明 |
|---|---|---|
Array(数组) |
✅ 正常工作 | 最常见的场景 |
Set |
✅ 正常 | 每次迭代产出一个元素 |
Map |
✅ 正常 | 每次迭代产出 [key, value] 二元组,取值需用 item[0]/item[1] |
实现了 [Symbol.iterator] 的自定义对象 |
✅ 正常 | 视具体实现 |
string |
❌ NG02200 | 报 "of type 'string'" |
普通对象字面量 {...} |
❌ NG02200 | 报 "of type 'object'",并提示改用 keyvalue pipe |
null / undefined |
⚠️ 不报错 | 渲染为空,见上文测试用例 |
Promise 实例(未 await/async) |
❌ NG02200 | 因为 Promise 本身不可迭代 |
也就是说:普通 JavaScript 对象不是可迭代的。文档给出的正例是 Array、Set、Map,而最常见的翻车点正是把一个对象字面量或 Promise 直接塞进了 ngForOf。
常见复现场景与修复方法
场景一:组件属性被赋值成了字符串/对象
// bad: 本应是数组,却是字符串或对象
items = 'whaaa';
// items = {a: 1, b: 2}; 同样会抛 NG02200
<!-- 抛出 NG02200 -->
<li *ngFor="let item of items">{{ item }}</li>
修复:保证类型是真正的可迭代集合。 若业务数据在对象上,先转换成数组再绑定:
// ok
items = ['whaaa', 'hello', 'angular'];
// 把对象的“值”作为列表
items = Object.values(someRecord);
// 把对象的“键”作为列表
keys = Object.keys(someRecord);
场景二:真的要遍历对象的键/值——改用 KeyValuePipe
文档明确给出的正解是 keyvalue 管道。它把 Object(或 Map)转换成由 {key, value} 项组成的数组,交给 *ngFor 消费,实现见 keyvalue_pipe.ts(name: 'keyvalue'),行为测试位于 keyvalue_pipe_spec.ts。
<!-- 遍历对象属性,entry.key / entry.value 可用 -->
<div *ngFor="let entry of userProfile | keyvalue">
<span>{{ entry.key }}:</span> <span>{{ entry.value }}</span>
</div>
场景三:异步数据未落地
使用 async 管道时确保管道后仍是集合,而不是对象:
<!-- ok:async 解析后是数组 -->
<li *ngFor="let user of users$ | async">{{ user.name }}</li>
<!-- 若后端返回 {list: [...]} 这样的对象外壳,需在组件里先做映射 -->
<!-- items = response.list -->
场景四:给长列表加上 trackBy,避免无谓 DOM 重建
*ngFor 默认按对象身份(identity)跟踪元素;数据从服务端重新拉取后即使内容没变,也会整体销毁重建 DOM。NgForOf 提供 ngForTrackBy 输入,通过自定义 TrackByFunction 用业务主键跟踪(源码与使用说明见 ng_for_of.ts):
<li *ngFor="let user of users; trackBy: trackById">{{ user.name }}</li>
trackById(index: number, user: User): number {
return user.id;
}
此外 NgForOfContext 还导出了 index、count、first、last、even、odd 等本地变量(见 ng_for_of.ts),可在模板中用 let i = index 等语法取用。
推荐升级:改用 @for 块
值得一提的是,源码中的 NgForOf 已经标记了弃用信息:@deprecated 20.0,官方建议改用内置的 @for 块(意图在未来的主版本中移除,见 ng_for_of.ts 与 ng_for_of.ts 的注释)。因此新代码中处理 NG02200 的"最终方案",是彻底告别 *ngFor:
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>列表为空时的占位内容</li>
}
要点(详见 控制流指南):
@for语法上要求必须提供track表达式,用于在数据变化时最小化 DOM 操作;有唯一标识(如id/uuid)就用标识,没有稳定标识的静态集合可用$index,一般不建议直接 track 整个对象引用;- 与
*ngFor不同,@for优先复用视图:当被 track 的属性变化而对象引用不变时,它只更新绑定(包括组件输入),而不是销毁重建整个元素; - 与
keyvalue管道组合依然成立:@for (entry of userProfile | keyvalue; track entry.key)。
继续排查的资源入口
- 官方错误文档原文:adev/src/content/reference/errors/NG02200.md;
- 全部错误码清单(NG02200 位于其中):adev/src/content/reference/errors/overview.md;
- 指令实现:packages/common/src/directives/ng_for_of.ts;
- differ 注册与查找机制:packages/core/src/change_detection/differs/iterable_differs.ts、packages/core/src/change_detection/differs/default_iterable_differ.ts;
- 错误码定义:packages/common/src/errors.ts;
- 行为测试(含报错文案断言):packages/common/test/directives/ng_for_spec.ts。
一句话总结:NG02200 是 Angular 在提示你"*ngFor 只认可迭代集合"。先确认绑定值在运行时确实是数组等可迭代对象;若要遍历对象属性,请交给 keyvalue 管道;新项目则直接使用带 track 的 @for 块,从根源上规避这类问题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00