公司动态
HarmonyOS Repeat 状态串行怎么办:中式美食长列表 key、复用和行内状态怎么拆
问题先说清楚列表页最怕一种问题筛选前第二行是展开的筛选后展开状态跑到了别的行购物清单里只勾选了“鸡蛋”结果“番茄”也像被勾上了排序后卡片上的局部输入框还保留着上一条数据的草稿。这类问题看起来像 ArkUI 列表复用不稳定实际通常是三件事混在了一起列表项身份没有稳定下来key 用了 index多行数据共享了同一个对象引用行内临时 UI 状态和数据对象状态没有分层。Repeat能帮助列表复用但它不能替你判断哪一行到底是谁。身份错了复用越积极问题越明显。先区分三个状态状态类型放在哪里更合适典型例子数据对象状态数据模型里菜谱 id、名称、价格、是否收藏行内临时 UI 状态以稳定 id 建表保存展开、输入框草稿、临时高亮页面筛选状态页面或父组件里关键词、分类、排序、页码我一般不会把这些都塞进一个item里。item代表数据本身展开状态这种东西更多是“这一行在当前页面怎么显示”。如果把它和数据模型混在一起筛选、排序、缓存、复用之后就很难排查。案例一用 index 当 key筛选后状态跑到别的行先看问题写法Repeat(this.recipes).each((item:RecipeItem,index:number){RecipeRow({item:item})}).key((item:RecipeItem,index:number)index.toString())这段代码在静态列表里看不出问题。问题发生在列表会过滤、删除、排序的时候。假设原来有三行indexidnameexpanded0r-001宫保鸡丁false1r-002番茄牛腩true2r-003鱼香肉丝false这时删掉第一行r-002从 index 1 变成 index 0。框架看到 key 也变了就很难知道“原来展开的是 r-002而不是现在的第 1 个位置”。结果可能变成indexidnameexpanded0r-002番茄牛腩false1r-003鱼香肉丝true展开状态跑到了r-003上这就是典型的状态串行。更稳的写法是用稳定业务 idRepeat(this.recipes).each((item:RecipeItem){RecipeRow({item:item,expanded:this.expandedMap.get(item.id)??false,onToggle:()this.toggleExpanded(item.id)})}).key((item:RecipeItem)item.id)行内临时状态不要依赖 index而是用id - state保存StateexpandedMap:Mapstring,booleannewMap()toggleExpanded(id:string){constnextnewMap(this.expandedMap)next.set(id,!(next.get(id)??false))this.expandedMapnext}这里有一个细节我会新建一个Map再赋值而不是直接改旧Map。这样更容易让页面状态变化被识别也避免后面排查时搞不清楚引用有没有变。案例二多行共享同一个对象引用勾选状态一起变第二个问题更隐蔽。比如构造购物清单数据时为了省事写了一个默认对象constdefaultMeta:IngredientMeta{checked:false,note:}constrows:IngredientRow[]ingredients.map((item)({id:item.id,name:item.name,meta:defaultMeta}))这段代码的问题是每一行拿到的是同一个defaultMeta引用。你改第一行rows[0].meta.checkedtrue第二行也会受影响因为它们指向的是同一份对象。列表复用一参与进来表现就更像“组件把状态串了”。正确做法是每一行生成独立对象constrows:IngredientRow[]ingredients.map((item)({id:item.id,name:item.name,meta:{checked:false,note:}}))如果后面要让对象字段被观察再按状态管理 V2 的方式声明对象字段不要靠共享引用偷懒ObservedV2classIngredientMeta{Tracechecked:booleanfalseTracenote:string}constrowsingredients.map((item)({id:item.id,name:item.name,meta:newIngredientMeta()}))本地怎么验证我用脚本复现了两个问题。第一组验证 index keyconstshiftedcaseIndexKeyStateShift()conststablecaseStableKeyKeepsIdentity()console.log(shifted)console.log(stable)验证结果很直观用 index 当 key 时筛选后展开状态跑到了另一行用稳定 id 当 key 时展开状态还跟着原来的r-002。第二组验证对象引用constsharedcaseSharedObjectReference()constseparatedcaseSeparateObjectReference()console.log(shared)console.log(separated)共享对象时两行都会变成checkedtrue独立对象时只有第一行变化。我会怎么改列表代码我的处理顺序是先检查key所有会筛选、排序、删除、插入的列表都不要用 index 当 key。再检查对象引用默认对象、缓存对象、复制对象都要确认是不是同一份引用。把行内临时 UI 状态从数据对象里拆出来用稳定 id 管。数据对象字段确实需要被观察时再用ObservedV2和Trace。最后再看列表性能比如Repeat、组件复用、图片加载、分页加载。顺序不要反。先谈性能容易把身份问题掩盖掉先把身份和状态边界拆清楚再谈复用排查会简单很多。和组件冻结、同步刷新有什么区别这篇讲的是“这一行到底是谁”。组件冻结解决的是“不可见组件要不要参与刷新”同步刷新解决的是“这一批状态什么时候刷新给后续 UI 动作”。它们能配合但不能互相替代。如果 key 错了组件冻结救不了状态串行如果对象引用共享了applySync也只是更快地把错误刷新出来。列表问题先查身份再查刷新再查性能这个顺序更稳。验证记录本地验证脚本article105-repeat-key-reuse-demo.mjs验证通过点index key 场景能复现筛选后的展开状态错位稳定 id key 能保持行身份共享对象引用能复现多行勾选一起变化每行独立对象能隔离勾选状态。后续放到完整 ArkUI 页面里还可以补一轮真机截图把筛选前后每一行的展开状态和勾选状态截出来。这样读者不仅能看到代码也能看到问题发生和修复后的界面变化。