补充:Repeat 虚拟滚动与 cachedCount 到底怎么用

发布时间:2026/6/10 13:18:21
补充:Repeat 虚拟滚动与 cachedCount 到底怎么用
补充Repeat 虚拟滚动与 cachedCount 到底怎么用上一篇我们已经讲了ForEach、LazyForEach和Repeat的区别也给出了一个结论在 HarmonyOS 6.0 的新项目里列表渲染可以优先考虑Repeat。但是在实际写聊天列表、商品列表、卡片流的时候很容易把两个概念搞混Repeat.virtualScroll()List.cachedCount()Repeat.template(..., { cachedCount })这几个名字里都出现了“虚拟滚动”“缓存”“懒加载”这些词但它们负责的事情并不一样。这篇补充就专门把这个点讲清楚。一、先说结论Repeat 不直接负责“上下预加载几条”如果你想实现这样的效果当前屏幕最多展示 6 条数据希望用户滚动时屏幕上方和下方附近可以提前准备 3 条减少快速滚动时的白屏和卡顿。正确思路不是只写Repeat.template(..., { cachedCount: 3 })而是List(){RepeatMessage(this.messageList).virtualScroll({totalCount:this.messageList.length}).key((item:Message)item.id).each((ri:RepeatItemMessage){ListItem(){MessageItem({message:ri.item})}})}.cachedCount(3)这里真正控制“可视区域外预加载”的是.cachedCount(3)也就是List的cachedCount不是Repeat.template的cachedCount。可以简单记成一句话Repeat.virtualScroll()负责开启虚拟滚动List.cachedCount()负责可视区域外预加载Repeat.template(..., { cachedCount })负责模板节点复用缓存池。二、.virtualScroll()做了什么先看这段代码RepeatMessage(this.messageList).virtualScroll({totalCount:this.messageList.length})它的意思是开启Repeat的虚拟滚动模式让列表不要一次性创建所有 item而是根据滚动容器的可视区域按需创建和复用 UI 节点。也就是说如果你的聊天列表有 1000 条消息它不会一开始就把 1000 个 UI 节点全部创建出来。更准确的理解是屏幕内的数据需要渲染 屏幕附近的数据可能会根据缓存策略提前渲染 离屏幕很远的数据不会提前创建 UI 节点所以不要把virtualScroll理解成“只渲染屏幕内 6 条”。更准确的说法是virtualScroll会让Repeat按可视区域附近的数据进行懒加载和节点复用而不是全量渲染整个数组。三、List.cachedCount(3)才是预加载附近 item假设现在一个页面最多展示 6 条消息每条消息高度大约是72vp希望上下附近额外预加载 3 条可以这样写interfaceMessage{id:stringtype:stringcontent:string}ComponentV2struct ChatListDemo{LocalmessageList:Message[][{id:1,type:text,content:第一条消息},{id:2,type:text,content:第二条消息},{id:3,type:text,content:第三条消息},{id:4,type:text,content:第四条消息},{id:5,type:text,content:第五条消息},{id:6,type:text,content:第六条消息},{id:7,type:text,content:第七条消息},{id:8,type:text,content:第八条消息},{id:9,type:text,content:第九条消息},{id:10,type:text,content:第十条消息}]build(){List(){RepeatMessage(this.messageList).virtualScroll({totalCount:this.messageList.length}).key((item:Message)item.id).each((ri:RepeatItemMessage){ListItem(){Row(){Text(ri.item.content).fontSize(16).fontColor(#222222)}.width(100%).height(72).padding({left:16,right:16}).backgroundColor(#FFFFFF)}})}.width(100%).height(72*6)// 一屏大约展示 6 条.cachedCount(3)// 可视区域外额外预加载附近 item}}这段代码里有两个关键点。第一个关键点是.virtualScroll({totalCount:this.messageList.length})它负责开启虚拟滚动。第二个关键点是.cachedCount(3)它挂在List上负责列表可视区域外的预加载数量。四、Repeat.template(..., { cachedCount })不是预加载上下几条再看另一种写法.template(confirm_trip,(ri:RepeatItemChatMessage){ListItem(){ConfirmTripCardComp({card:ri.item.card})}},{cachedCount:2})这里的cachedCount: 2很容易被误解成“屏幕外缓存 2 条卡片数据”。其实不是。它更准确的含义是confirm_trip这个模板类型最多缓存 2 个可复用的 UI 节点。也就是说它是模板复用层面的缓存池不是列表可视区域外的预加载数量。可以这样区分写法控制什么可以怎么理解Repeat.virtualScroll()是否开启虚拟滚动不一次性创建全部 itemList.cachedCount(3)可视区域外预加载 item屏幕附近多准备一些列表项Repeat.template(..., { cachedCount: 2 })某个模板的复用节点缓存池某类模板最多保留几个可复用节点所以如果你的目标是“用户滚动时上下附近提前加载 3 条”应该写在List上List(){// Repeat ...}.cachedCount(3)而不是只写.template(xxx,builder,{cachedCount:3})五、多类型聊天消息里的完整写法如果你的聊天列表里既有普通文本消息又有确认打车卡片可以这样组织interfaceTripCard{title:stringstartLocation:stringendLocation:stringdistance:stringduration:stringpriceRange:string}interfaceChatMessage{id:stringtype:stringcontent:stringcard?:TripCard}ComponentV2struct ChatListComp{Parammessages:ChatMessage[]build(){List(){RepeatChatMessage(this.messages).virtualScroll({totalCount:this.messages.length}).key((item:ChatMessage)item.id).templateId((item:ChatMessage){if(item.typeconfirm_trip){returnconfirm_trip}returntext}).each((ri:RepeatItemChatMessage){ListItem(){Text(ri.item.content).fontSize(15).fontColor(#222222).padding(12)}}).template(confirm_trip,(ri:RepeatItemChatMessage){ListItem(){if(ri.item.card){ConfirmTripCardComp({card:ri.item.card})}}},{cachedCount:2})}.width(100%).height(100%).cachedCount(3)}}这段代码里有三层含义。第一层.virtualScroll({totalCount:this.messages.length})开启虚拟滚动。第二层.templateId((item:ChatMessage){if(item.typeconfirm_trip){returnconfirm_trip}returntext})根据消息类型选择模板。普通文本走默认模板确认打车消息走confirm_trip模板。第三层.cachedCount(3)挂在List上表示可视区域外额外预加载附近列表项。而这里.template(confirm_trip,...,{cachedCount:2})只是confirm_trip模板自己的节点复用缓存池不是上下预加载 2 条。六、结合聊天列表再理解一次以聊天列表为例假设当前屏幕能看到 6 条消息列表总共有 100 条消息。如果不做虚拟滚动可能会倾向于一次性创建大量 UI 节点数据量大时会影响性能。使用Repeat.virtualScroll()后系统会根据列表当前显示区域按需创建附近的 UI 节点。再加上List().cachedCount(3)可以让滚动容器在可视区域外提前准备一些 item。大致可以这样理解当前屏幕可见约 6 条 屏幕附近预加载由 List.cachedCount(3) 控制 离屏幕很远的数据不会提前创建 UI 节点 滚动过程中复用已经创建过的节点不过要注意实际运行时不一定永远严格等于“上面 3 条 下面 3 条”。因为缓存分布还会受下面这些因素影响当前是否在列表顶部当前是否在列表底部用户滚动方向item 高度是否一致外层容器布局系统内部复用策略所以在表达时不要说死一定是上面 3 条下面 3 条。更稳的说法是设置List.cachedCount(3)后列表会在可视区域外额外预加载附近 item减少滚动时的白屏和卡顿。七、开发时最容易写错的地方1. 只写了virtualScroll以为已经设置了缓存数量错误理解RepeatMessage(this.messageList).virtualScroll({totalCount:this.messageList.length})然后以为它已经自动设置了“上下缓存 3 条”。实际上virtualScroll只是开启虚拟滚动不是设置上下缓存数量。如果要控制可视区域外预加载数量还要看外层滚动容器List(){// Repeat ...}.cachedCount(3)2. 把template cachedCount当成列表预加载数量错误理解.template(confirm_trip,builder,{cachedCount:3})以为这代表“确认打车卡片在屏幕外预加载 3 条”。实际上它表示的是confirm_trip模板类型最多缓存 3 个可复用节点。3. 没有写稳定的 key列表数据经常新增、删除、更新时建议给Repeat配置稳定的key.key((item:ChatMessage)item.id)这样系统在更新 UI 时更容易识别每条消息减少不必要的重建。聊天列表里不要用数组下标当核心身份标识尤其是消息可能插入、删除、刷新时。八、最终记忆口诀最后用几句话记住就够了ForEach小列表普通循环。 LazyForEach老的大列表懒加载方案写法偏重。 Repeat新的统一循环渲染方案。 Repeat.virtualScroll开启虚拟滚动。 List.cachedCount控制可视区域外预加载 item。 Repeat.template cachedCount控制某个模板类型的节点复用缓存池。如果是 HarmonyOS 6.0 新项目里的聊天列表我会优先这样写List(){RepeatChatMessage(this.messages).virtualScroll({totalCount:this.messages.length}).key((item:ChatMessage)item.id).templateId((item:ChatMessage)item.type).each((ri:RepeatItemChatMessage){ListItem(){TextMessageItem({message:ri.item})}}).template(confirm_trip,(ri:RepeatItemChatMessage){ListItem(){ConfirmTripCardComp({card:ri.item.card})}},{cachedCount:2})}.cachedCount(3)这套写法的优势是列表不会全量渲染所有消息可以根据type分发不同消息模板滚动时可以预加载附近 item复杂卡片模板可以复用节点很适合 AI 聊天、订单卡片、商品流、通知流这类多类型列表场景九、一句话总结Repeat负责循环渲染和虚拟滚动List.cachedCount负责可视区域外预加载Repeat.template cachedCount负责模板节点复用。想让聊天列表“当前屏幕 6 条、上下附近预加载 3 条”核心写法就是List(){RepeatMessage(this.messageList).virtualScroll({totalCount:this.messageList.length}).key((item:Message)item.id).each((ri:RepeatItemMessage){ListItem(){MessageItem({message:ri.item})}})}.cachedCount(3)