鸿蒙 ArkUI 组件复用指南:@Reusable 装饰器与 NodePool 方案
鸿蒙 ArkUI 组件复用通过@Reusable 装饰器实现,将移除组件放入缓存池以减少创建销毁开销。主要场景包括同列表项复用、多类型列表项复用及跨列表复用。基础开发需标记组件并实现 aboutToReuse 回调。对于复杂跨列表场景,可自定义 NodePool 全局缓存池管理节点生命周期。该技术能显著提升长列表滑动流畅度与响应速度,是优化 ArkUI 应用性能的关键手段。

鸿蒙 ArkUI 组件复用通过@Reusable 装饰器实现,将移除组件放入缓存池以减少创建销毁开销。主要场景包括同列表项复用、多类型列表项复用及跨列表复用。基础开发需标记组件并实现 aboutToReuse 回调。对于复杂跨列表场景,可自定义 NodePool 全局缓存池管理节点生命周期。该技术能显著提升长列表滑动流畅度与响应速度,是优化 ArkUI 应用性能的关键手段。

组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。
核心价值:
组件复用适用于任何发生自定义组件销毁和再创建的场景:
ArkUI 通过 @Reusable 装饰器实现组件复用,其核心机制如下:
图:@Reusable 组件从移除到缓存再到复用的完整流程
工作流程:
@Reusable 的组件在离开屏幕后,从组件树移除并放入 CustomNode 虚拟节点实现组件复用需要遵循三个基本步骤:
// 1. 定义可复用组件
@Reusable
@Component
struct ReusableComponent {
@State text: string = ''
// 2. 实现复用回调
aboutToReuse(params: Record<string, Object>): void {
this.text = params.text as string
}
build() {
// 组件构建逻辑
}
}
@Entry
@Component
struct Index {
@State switch: boolean = true
@State typeStr: string = 'typeA'
build() {
Column() {
// 3. 布局中使用并设置 reuseId
if (this.switch) {
ReusableComponent({ text: this.typeStr }).reuseId(this.typeStr)
}
}
}
}
关键注意事项:
@Reusable 修饰的组件需要布局在同一个父自定义组件下才能实现缓存复用@Reusable 组件中嵌套使用另一个 @Reusable 组件reuseId 时,组件名会默认作为 reuseId当列表中所有项具有相同结构时,可以将整个列表项作为复用单位。
图:结构相同的列表项滑动复用示意图
实现方案:
@Component
export struct OneTypeItemPage {
private dataSource: DataSource // 数据源
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
// 使用相同 reuseId 标记同类组件
ItemView({ title: item.title, from: item.from, tail: item.tail }).reuseId('item_id')
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
@Reusable
@Component
struct ItemView {
@State title: string | Resource = ''
@State from: string | Resource = ''
@State tail: string | Resource = ''
aboutToReuse(params: Record<, >): {
. = params.
. = params.
. = params.
}
() {
}
}
当列表中包含多种结构类型的项时,需要为每种类型分别设置复用逻辑。
图:文本、单图、多图等不同类型列表项的复用分组
实现方案:
@Component
export struct MultiTypeItemPage {
private dataSource: DataSource
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
// 根据数据类型选择不同组件类型
if (item.type === 0) {
TextTypeItemView({ item: item }).reuseId('text_item_id')
} else if (item.type === 1) {
ImageTypeItemView({ item: item }).reuseId('image_item_id')
} else if (item.type === 2) {
ThreeImageTypeItemView({ item: item }).reuseId('three_image_item_id')
}
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
// 不同类型的组件分别定义
@Reusable
@Component
struct TextTypeItemView {
}
struct {
}
struct {
}
当列表项有共同部分和差异部分时,可以将子组件拆分,通过组合实现不同类型。
图:通过顶部、中部、底部子组件组合成不同类型的列表项
实现关键:使用 @Builder 而非嵌套自定义组件,确保所有可复用组件位于同一缓存池。
@Component
export struct ComposableItemPage {
// 使用@Builder 组合子组件
@Builder itemBuilderSingleImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
@Builder itemBuilderThreeImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
Column() {
// 根据类型选择不同的 Builder 组合
(item. === ) {
.(item)
} (item. === ) {
.(item)
}
}
}
}, item..())
}
}
}
}
}
struct {
}
struct {
}
struct {
}
为什么使用@Builder:缓存池位于自定义组件上,嵌套子组件会分割缓存池导致复用失效。@Builder 可以使内部自定义组件汇聚在同一缓存池。
在 Swiper+List 实现的页签切换场景中,不同页面的列表可能包含结构相同的列表项,但默认机制无法跨页面复用。
图:News、Hot 等不同页签下相同结构列表项的跨列表复用
技术挑战:每个列表项的父组件是各自的 List,当 Swiper 切换页面时,无法直接复用上一个页面的列表项。
为什么选择 Swiper+List 而非 Tabs+List:
LazyForEach(),只能使用 ForEach+TabContentaboutToDisappear(),无法回收组件通过自定义 NodePool 工具类,利用 BuilderNode 的节点复用能力实现跨列表组件复用。
图:NodePool 全局管理节点创建、回收和复用的完整架构
export class NodeItem extends NodeController {
public builder: WrappedBuilder<ESObject> | null = null
public node: BuilderNode<ESObject> | null = null
public data: ESObject = {}
public type: string = ''
public id: number = 0
// 组件消失时回收到缓存池
aboutToDisappear(): void {
NodePool.getInstance().recycleNode(this.type, this)
}
// 更新节点数据
update(data: ESObject) {
this.data = data
this.node?.reuse(data)
}
// 创建或更新节点
makeNode(uiContext: UIContext): FrameNode | null {
if (!this.node) {
. = (uiContext)
..(., .)
} {
.(.)
}
..()
}
}
export class NodePool {
private static instance: NodePool
private idGen: number
private nodePool: HashMap<string, LinkedList<NodeItem>>
private constructor() {
this.nodePool = new HashMap()
this.idGen = 0
}
// 单例模式确保全局唯一缓存池
public static getInstance(): NodePool {
if (!NodePool.instance) {
NodePool.instance = new NodePool()
}
return NodePool.instance
}
// 获取可复用节点
public getNode(type: string, item: ESObject, builder: WrappedBuilder<ESObject>): NodeItem | undefined {
let nodeItem: NodeItem | =
(..()) {
( i = ; i < ..()?.; i++) {
: | = ..()?.(i)
(!tmpItem.?.()?.()) {
nodeItem = tmpItem
..()?.(i)
}
}
}
(!nodeItem) {
nodeItem = ()
nodeItem. = builder
nodeItem. = item
nodeItem. =
nodeItem. = .()
}
nodeItem
}
() {
node. = {}
(!..()) {
..(, ())
}
..()?.(node)
}
}
组件复用是优化 ArkUI 应用性能的关键技术,尤其对于包含长列表、复杂条件渲染的场景。通过合理运用 @Reusable 装饰器和 aboutToReuse 生命周期,可以显著提升界面流畅度和响应速度。
对于简单场景,内置的复用机制足以满足需求;对于复杂的跨列表复用场景,自定义 NodePool 方案提供了灵活的解决方案。开发者应根据实际业务需求选择合适的复用策略,在提升性能的同时确保代码的可维护性。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online