LazyForEach 的 key 我填错了,列表刷新时闪得像电视雪花
先上代码,后面解释我为什么这么写。
// 这是我一开始写的,看起来没毛病对吧?LazyForEach(this.dataSource,(item:Item,index:number)=>{ListItem(){MyCard({item:item})}},(item:Item)=>JSON.stringify(item))// key 生成器问题就出在那个JSON.stringify(item)上。
当时项目里有个消息列表,数据量倒也不大,就两百来条。需求加了个下拉刷新,我一拉,整个列表开始疯狂闪烁,每一行都在重建,复选框状态全丢了,头像加载的占位图重新转圈圈。那场面,跟老式电视机信号不好时的雪花屏差不多。
我第一反应是刷新逻辑写错了。this.dataSource.reloadData(newList)是不是不应该全量替换?是不是应该做增量 diff?我跑去翻官方文档,文档里只写了"数据变化时触发 item 重建",但没告诉我什么情况下会全量重建。
排查了两天。第一天我怀疑是DataSource的实现问题,重写了一遍IDataSource接口,没解决。第二天我把notifyDataReload换成了notifyDataDelete+notifyDataAdd,列表确实不闪了,但性能更差,因为删了再加等于走了两遍流程。
你猜怎么着?问题根本不在刷新方式上。我在 DevEco 的 Profiler 里抓了一帧,发现每次刷新,所有ListItem的aboutToAppear全被执行了一遍。两百个 item,两百次重建。
最后我把目光锁定了 key 生成器。JSON.stringify(item)的问题在于,item 对象里有个timestamp字段,每次刷新服务端都会返回新的时间戳。JSON.stringify一跑,每个 key 都变了。鸿蒙的渲染引擎一看 key 全换了,理所当然地认为是全新数据,全部重建。
说白了,key 的作用就是告诉框架"这条数据没变,复用之前的组件"。如果 key 变了,框架就会销毁旧的、创建新的,不管数据内容是不是真的不同。
修复后的代码长这样:
LazyForEach(this.dataSource,(item:Item,index:number)=>{ListItem(){MyCard({item:item})}},(item:Item)=>item.id)// 用业务唯一标识就这么一行改动。item.id是数据库主键,刷新前后不会变。我重新编译,下拉刷新,列表稳稳的,一个aboutToAppear都没多触发。
顺手测了一组数据:
| 场景 | key 策略 | 刷新耗时 | 重建 item 数 |
|---|---|---|---|
| 200 条消息列表 | JSON.stringify(item) | 约 420ms | 200 |
| 200 条消息列表 | item.id | 约 35ms | 仅变更项 |
| 500 条长列表 | JSON.stringify(item) | 直接卡死 1.5s | 500 |
| 500 条长列表 | item.id | 约 60ms | 仅变更项 |
差距是十倍起步。数据量大的时候,JSON.stringify 本身就有序列化开销,再加上全量重建,不卡才怪。
等一下,这里我漏说一个前提。如果你用index当 key,比如(item, index) => index.toString(),情况会更隐蔽。列表尾部追加数据时没问题,但中间插入一条,后面所有 index 都变了,结果还是大面积重建。我见过有人用 index 当 key,然后在列表顶部插入一条新消息,整个列表闪了一下,跟我的雪花屏异曲同工。
正确的做法只有一个:用业务层面的唯一标识。没有 id 的话,自己根据数据内容构造一个稳定的 hash,但千万别把会变的字段包进去。
我还顺手写了个小工具函数,项目里复用:
functionstableKey(item:Record<string,any>,fields:string[]):string{returnfields.map(f=>String(item[f]??'')).join('|')}// 用法:stableKey(item, ['id', 'type'])好处是强制你显式声明哪些字段参与 key 计算,不会再手滑把timestamp之类的动态字段带进去。
顺便说一句,鸿蒙文档里关于 LazyForEach 的 key 说明藏得挺深的,在"高级组件"章节的一个小注脚里,我第一次读完全没注意到。要不是这次踩坑,我估计到现在还觉得JSON.stringify是个偷懒的好办法。
反正我以后不会再用 JSON.stringify 当 key 了。你要是也在用 LazyForEach,现在就去检查一下你的 key 生成器。
本文遵循 MIT 协议,转载请注明出处。