资讯动态

鸿蒙6.0 Repeat组件:从ForEach迁移到键值追踪的高性能列表渲染

发布时间:2026/10/16 12:54:00 来源:尧图企业网站定制
做鸿蒙6.0应用开发的朋友应该对 ArkUI 里的列表渲染不陌生。列表页几乎每个应用都绕不开而这次我想专门聊的是 Repeat 组件——它真正解决了我在频繁增删数据场景下遇到的卡顿和渲染错乱问题。先说结论Repeat 不是简单的 ForEach 换皮它是基于键值追踪的高性能循环渲染组件适合数据变化频繁、列表项复用好、状态需要稳定的场景。这篇文章我会从实际遇到的一个信息展示场景出发把 Repeat 的用法、键值规则、状态管理配合、性能调优和踩坑经验一次讲透。不管你是刚从 ForEach 迁移还是想在鸿蒙6.0里优化现有列表这篇都值得看完。1. 列表刷新卡顿背后的原因为什么我把 ForEach 换成了 Repeat1.1 出问题的场景频繁增删的中等列表当时要处理一个通知中心类的页面数据来自服务端推送用户在页面上还能手动插入、删除、标记已读。列表规模其实不大撑死两三百条但操作非常频繁几乎每秒都可能有一条数据进来。用 ForEach 渲染之后滑动过程中能明显感觉到掉帧删除一条时还会出现短暂的闪动偶尔甚至出现“删了 A 结果 B 消失了”这种诡异现象。我一开始以为是业务代码问题比如数组操作不对、数据源有脏数据。后来在子组件里加了生命周期日志才发现问题根本不在业务层而是 ForEach 在数据变化时做了大量无意义的组件重建。每次插入或删除框架都要重新比对一大批子节点计算量随着列表长度线性上升UI 线程自然吃紧。这个场景非常典型列表不长但变化频繁。如果只是几千条数据用 LazyForEach 能解决懒加载但中等长度、高频修改的列表反而最考验渲染组件的 diff 和复用能力。Repeat 就是冲着这个场景来的。1.2 ForEach 的更新机制从 Diff 到组件复用的缺陷ForEach 的逻辑是数据源变化后对每一项执行一次 diff找出哪些子组件需要更新、插入或删除。听起来很合理但实际执行时有两个问题。第一ForEach 的 diff 粒度比较粗。只要传入数据源整体发生变化它就倾向于把整个数组重新过一遍即使你只删了中间一条它也要重新评估后面所有的项。对于两三百条的列表单次操作可能就是几百次比对和大量子组件重新构建叠加频繁操作后性能就崩了。第二子组件复用能力弱。ForEach 在渲染时会根据 key 判断是否复用但如果你没传 key 或者用了 index 当 key那么删除中间一项后后续所有项的 index 都会变化框架会认为这些组件都“变了”然后逐个重建。结果就是界面闪动、滚动位置丢失、内部状态错乱。所以 ForEach 更适合渲染稳定、变化频率低的列表。一旦数据进入高频增删模式它就有点力不从心了。1.3 鸿蒙6.0 的 Repeat 到底改了什么Repeat 在设计上和 ForEach 最大的区别是把“哪一项需要重建”这件事交给了键值机制。它要求你为每个数据项提供一个唯一且稳定的 id 生成函数框架通过这个 key 精确追踪每一项。当数据源变化时Repeat 会比对 key 集合哪些 key 是新增的哪些 key 被删除了哪些 key 依然存在。key 没变的子组件直接复用不重新执行构建流程只有 key 新增或变化的项才会真正创建新组件。这个机制避免了 ForEach 那种大面积无效 diff性能提升非常明显。我在调试环境里做过简单对比同样两百条数据的列表连续插入删除二十次ForEach 的组件创建日志几乎刷满屏幕Repeat 的创建次数少了很多滑动帧率也稳定多了。而且 Repeat 的代码写起来更直观因为子组件和数据项的对应关系非常明确。2. Repeat 组件上手实操参数、键值与最简 Demo2.1 最简示例把 ForEach 改成 Repeat 只需要三步先看一个最简单的例子。假设有一个字符串数组要渲染成一组文本const data: string[] [A, B, C] Column() { Repeat({ data: data, id: (item: string) item }) (item: string) { Text(item) .fontSize(20) .padding(10) } }对比一下原来的 ForEach 写法Column() { ForEach(data, (item: string) { Text(item) .fontSize(20) .padding(10) }, (item: string) item) }结构上非常接近Repeat 接收一个包含data和id的配置对象然后在后面直接写子组件构建函数。data是要循环的数据源id是键值生成器构建函数里通过形参拿到当前项。这里有三点需要注意。第一id函数返回的必须是字符串不能是数字。第二构建函数写在 Repeat 配置对象的右括号后面是尾随 lambda 的写法。第三Repeat 必须放在容器组件里使用比如 Column、List、Grid不能单独作为根节点。2.2 id 键值Repeat 性能的灵魂Repeat 的所有性能优势都建立在 key 正确的前提上。key 必须满足三个要求唯一、稳定、可预测。先说唯一性。id 函数返回的字符串在整个列表里不能重复。如果两条数据生成了相同的 key框架会警告而且复用逻辑会错乱可能出现“两条数据共用一个子组件”的情况。再说稳定性。同一个数据项在不同渲染周期里key 必须保持不变。最常见的错误是用 index 当 keyRepeat({ data: list, id: (item: Item, index: number) ${index} })列表是[A, B, C]时index 分别是 0、1、2。删除中间的 B 之后列表变成[A, C]index 变成 0 和 1。原来的 key 1 对应 B现在 key 1 对应 C框架会觉得 C 是从 B“变”过来的于是复用了 B 的组件但数据对不上状态也乱七八糟。正确的做法是用业务 ID比如通知 ID、订单号、用户 ID。如果数据源没有自然 ID可以在创建数据时生成一个自增字段存在对象里class Item { id: string name: string constructor(id: string, name: string) { this.id id this.name name } } Repeat({ data: this.list, id: (item: Item) item.id }) (item: Item) { Text(item.name) }这样无论列表怎么增删只要数据项不变它的 key 就不会变Repeat 才能精准复用组件。2.3 在 List 与 Grid 中使用 Repeat 的标准姿势Repeat 在滚动容器里的用法和 Column 里稍微不同因为要配合 ListItem 或 GridItem 使用。Entry Component struct ListRepeatDemo { State private items: Item[] [] build() { List({ space: 8 }) { Repeat({ data: this.items, id: (item: Item) item.id }) (item: Item, index: number) { ListItem() { Row() { Text(item.name) .fontSize(16) Blank() Text(${index 1}) .fontSize(12) } .padding(12) } } } .width(100%) .layoutWeight(1) } }注意一个关键点List 的直接子组件必须是 ListItem。所以 Repeat 的构建函数里返回的应该是 ListItem而不是普通的 Row 或 Text。Grid 同理构建函数里要包一层 GridItem。Scroll 容器也可以配 Repeat但 Scroll 本身没有列表项的回收复用机制列表项超出屏幕后不会像 List 那样自动回收。所以如果要做长列表滚动优先选 List 而不是 Scroll。Scroll 只适合条目少、不需要回收的场景。3. 进阶玩法Repeat 搭配 List、状态管理与动画3.1 Repeat、ForEach、LazyForEach 三兄弟怎么选很多同学拿到 Repeat 之后恨不得把所有循环都改成 Repeat。但实际项目中三个组件各有分工。组件渲染策略懒加载键值机制最佳场景ForEach全量循环渲染不支持可用 key但复用弱数据量小、变化少的静态列表Repeatkey 精准追踪 组件复用不支持必选 id复用强中等长度、频繁增删改的列表LazyForEach按需渲染、滚动回收支持需要数据源实现懒加载接口几百条以上的大数据列表我的选型建议是不超过几十条、几乎不变化的列表继续用 ForEach简单直接几十到几百条、操作频繁的列表用 Repeat收益最大超过几百条或者数据量不确定、需要滚动按需加载的用 LazyForEach别硬上 Repeat。Repeat 不是 LazyForEach 的替代品。它优化的重点是更新效率和组件复用数据量特别大时依然建议用 LazyForEach 做懒加载两者并不冲突。3.2 对象属性变化不刷新状态管理要和 Repeat 配合好Repeat 解决的是“哪一项要渲染”的问题但列表项内部字段变化能不能触发刷新取决于状态管理。这是很多新手最容易踩的坑。比如一个 NoticeItem 类里面有title和isRead两个字段。你直接在方法里写this.notices[0].isRead true页面经常不刷新。原因很简单Repeat 观察的是 key 集合的变化而对象内部属性是否被观察要看状态装饰器的粒度。推荐的做法是用 Observed 修饰数据类子组件用 ObjectLink 接收对象。Observed class NoticeItem { id: string title: string isRead: boolean constructor(id: string, title: string, isRead: boolean) { this.id id this.title title this.isRead isRead } } Component struct NoticeRow { ObjectLink item: NoticeItem build() { Row() { Text(this.item.title) if (this.item.isRead) { Text(已读) } else { Text(未读) } } } }有了 Observed 和 ObjectLink你直接修改item.isRead子组件就能感知到变化。如果你不想引入这套装饰器也可以修改后整体替换数组项this.notices[index] newItem。效果类似但每次都要手动创建新对象代码冗余一点。我的经验是只要列表项内部有可交互状态已读未读、选中、展开收起就老老实实上 Observed ObjectLink早用早省心。3.3 用动态 key 制造重建效果实现列表动画Repeat 的 key 稳定是性能的前提但有时候我们反而希望某个组件强制重建。比如列表项要重播一个入场动画或者数据没变但视觉效果需要重置。这时可以给 id 加一个版本号字段Repeat({ data: this.items, id: (item: Item) ${item.id}_${item.version} }) (item: Item) { ItemCard({ item: item }) }当你要强制刷新某项时给它的 version 加一。框架会认为 key 变了于是创建新的组件实例动画和状态都会重新初始化。删除动画也可以用 animateTo 包裹数据修改animateTo({ duration: 200, curve: Curve.EaseOut }, () { this.notices.splice(index, 1) })注意animateTo 只对参与动画的属性变化生效列表项如果配置了 transition 效果才能表现得更自然。但记住一个原则不要把随机数塞进 id 来制造动画效果那样会让 Repeat 的复用率直接归零性能优势全没了。4. 高频问题排查与避坑实录4.1 五大高频坑几乎每个都会遇到我把实际调试中遇到的典型问题整理成了一张速查表每个问题都对应明确的查错方向。现象原因解决方案列表项显示错乱内容张冠李戴id 重复或用了 index 当 key改为业务唯一 ID删除中间项后后面项的状态错位key 不稳定后续项 key 变化导致错误复用使用数据项自带的稳定 id修改对象属性页面不刷新状态观察粒度不够对象未用 Observed使用 Observed ObjectLink 或整体替换数组项异步数据返回后列表不更新数组未用 State 修饰或直接修改了原数组引用用 State 修饰数组重新赋值给新数组数据量一大还是卡Repeat 不具备懒加载全量渲染换 LazyForEach 或分页加载第一条最常见。服务端返回的数据没有 id开发时就图方便用 index 当 key后果就是列表项排序一变界面上的状态跟着乱跑。第二条最容易误判。当你用item.id当 key 时看起来没问题但如果服务端每次返回的同一条数据 id 不同或者客户端重新创建了对象key 就变了组件会重建状态自然丢失。第三条要记住Repeat 的 key 集合负责追踪项的增删项内部的字段变化依赖状态管理框架。两个机制必须配合缺一个都会觉得“Repeat 有问题”。4.2 怎么判断 Repeat 是否真正复用了组件排查 Repeat 性能问题时不能靠肉眼观察要用日志判断。在列表子组件的生命周期回调里加打印Component struct TestRow { aboutToAppear(): void { console.info(row create) } aboutToDisappear(): void { console.info(row destroy) } build() { Text(test) } }然后反复增删数据。如果 Repeat 工作正常你会发现row create的打印次数远小于数据变更次数大部分操作只是更新已有组件。如果每次增删数据创建日志刷满屏幕说明 key 不稳定或者数据对象每次都被重建了。我还会打印 id 序列来做对比id: (item: Item) { const key item.id console.info(current key: ${key}) return key }通过日志能直观看到每次更新时框架计算了哪些 key。如果 key 集合没有变化的项也被大量重新计算问题大概率出在数据源本身的引用变化上。4.3 一个接近生产环境的完整示例最后给一个完整示例模拟通知中心的新增、删除和统计功能把 Repeat 的主要用法串起来。class NoticeItem { id: string title: string time: string constructor(id: string, title: string, time: string) { this.id id this.title title this.time time } } Entry Component struct NoticePage { State private notices: NoticeItem[] [] private seed: number 0 addNotice() { this.seed const item new NoticeItem( notice_${this.seed}, 新通知 ${this.seed}, 刚刚 ) this.notices.unshift(item) } removeNotice(index: number) { animateTo({ duration: 200 }, () { this.notices.splice(index, 1) }) } build() { Column({ space: 12 }) { Row({ space: 8 }) { Button(新增通知) .onClick(() this.addNotice()) Text(共 ${this.notices.length} 条) .fontSize(14) Blank() } .width(100%) List({ space: 8 }) { Repeat({ data: this.notices, id: (item: NoticeItem) item.id }) (item: NoticeItem, index: number) { ListItem() { Row({ space: 12 }) { Column({ space: 4 }) { Text(item.title) .fontSize(16) Text(item.time) .fontSize(12) .fontColor(#999999) } Blank() Button(删除) .fontSize(12) .onClick(() this.removeNotice(index)) } .width(100%) .padding(12) .backgroundColor(#FFFFFF) .borderRadius(8) } } } .layoutWeight(1) .width(100%) } .padding(16) } }这个示例里有两个关键设计。第一id使用notice_${this.seed}每次新增都生成唯一字符串删除时 Repeat 能精准找到对应项并复用其他项。第二删除操作被 animateTo 包裹状态变更带动画过渡体验更自然。如果你的列表项内部有更多交互记得把数据类改成 Observed子组件抽出来用 ObjectLink 接收这样状态刷新才能和组件复用共存。5. 最后几个实操建议5.1 选型建议和团队规范在团队里定一套简单的渲染规范能省掉很多沟通成本。我常用的三条规则是小列表用 ForEach中等列表用 Repeat大数据量列表用 LazyForEach所有列表项的 key 必须来自业务唯一 ID禁止用 indexid 函数里禁止出现随机数、时间戳等每次渲染都会变化的值。代码评审时重点看两点。一是 key 是不是稳定二是数据对象内部状态是否用了正确的装饰器。只要这两点过关Repeat 的复用机制就能稳定发挥性能不会莫名其妙劣化。5.2 一个容易被忽略的小技巧如果 id 的生成逻辑比较复杂比如多个字段拼接建议把生成方法放到数据模型里而不是散落在每个 Repeat 的 id 函数中。class Item { id: string version: number get stableKey(): string { return ${this.id}_${this.version} } }然后在组件里直接调用item.stableKey既统一了 key 规则又避免多个地方拼错。需要强制重建时只改 version组件就能按预期重建。这个小技巧看起来不起眼但能把 Repeat 的复用率稳定在一个很高的水平。我在实际调试里感受最深的一点是Repeat 最值钱的不是某个性能指标而是它把“哪些列表项需要重建”这个复杂问题从开发者手里接走了。只要你把 key 规则定好UI 和数据的关系就不会乱。这个思路也值得沿用到后续所有列表场景里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑