1、组件复用概念

组件复用针对的是自定义组件,只要发生了相同自定义组件销毁和再创建的场景,都可以使用组件复用,例如滑动列表场景,会出现大量重复布局的创建,使用组件复用可以大幅度降低了因频繁创建与销毁组件带来的性能损耗。

1.1 组件复用实现方法

组件复用的实现方式主要有以下两种:

  • 系统提供的组件复用:使用@Reusable装饰器修饰自定义组件,使其具备组件复用能力。
  • 自定义组件复用:使用BuilderNode实现全局复用。

1.2 @Reusable组件复用的原理

系统提供的组件复用的行为,是将子组件放在父组件的复用缓存池里,缓存池是一个MapArray的数据结构,以reuseldkey,具有相同reuseld的组件在同一个List中,可以相互复用,reuseld默认是自定义组件的名字。

在这里插入图片描述

具体来说是:

  • 标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中。

  • 当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从复用缓存中查找可复用的组件节点。

  • 找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。
    原理图如下:
    在这里插入图片描述

  • CustomNode是一种自定义的虚拟节点,用来缓存列表中的某些内容,以提高性能和减少不必要的渲染。通过使用CustomNode,可以实现只渲染当前可见区域内的数据项,将未显示的数据项缓存起来,从而减少渲染的数量,提高性能。被回收的CustomNode对象将放在CachedRecycleNodes集合中。

  • RecycleManager是一种用于优化资源利用的回收管理器。当一个数据项滚出屏幕时,不会立即销毁对应的视图对象,而是将该视图对象放入复用池中。当新的数据项需要在屏幕上展示时,RecycleManager会从复用池中取出一个已经存在的视图对象,并将新的数据绑定到该视图上,从而避免频繁的创建和销毁过程。通过使用RecycleManager,可以大大减少创建和销毁视图的次数,提高列表的滚动流畅度和性能表现。

1.3 组件复用的约束和限制

不同复用组件(组件名不同或者reuseld不同)之间相同子组件无法复用,因为它们在缓存池的不同List中,如下图所示,子组件B之间无法复用。因为复用组件是以resusedId或者组件名称放在不同的list里的,下图中三个子组件B父组件不同,所以他们都会分配到不同的list不同复用缓存池中。
在这里插入图片描述
我们可以将上图进行如下改造,可以将复用组件改为@Builder函数,然后将子组件B使用@Reusable修饰,这样子组件的缓存池就会在父组件上共享:
在这里插入图片描述
总之默认的组件复用行为,是将子组件放在父组件的缓存池里,受到这个限制,不同父组件中的相同子组件无法复用,推荐的解决方案是将父组件改为builder函数,让子组件共享组件复用池。

2、复用组件的场景类型

2.1 标准型

标准型的复用是指复用组件之间布局完全相同,比如List列表中基本相同的Item布局,如下图:
在这里插入图片描述
因为该场景只有一个复用组件,所以在缓存中只有一个复用组件list。其父组件复用缓冲池如下:
在这里插入图片描述

2.2 有限变化型

复用组件之间布局有所不同,但是类型有限,复用思路有两个即:使用reuseId或者独立成不同自定义组件
在这里插入图片描述
详细来说是:

  • 场景A:类型1和类型2布局不同,业务逻辑不同。可以使用两个不同的自定义组件进行复用,给复用组件1和复用组件2设置不同的reuseId,此时组件复用池内的状态如下图所示,复用组件1和复用组件2处于不同的复用list中,注意该场景是不同组件配置不同的reuseId
    在这里插入图片描述
    示例代码如下:


struct ReuseType2A {
  // ...

  build() {
    Column() {
      List() {
        LazyForEach(this.dataSource, (item: number) => {
          ListItem() {
            if (item % 2 === 0) { // 模拟业务条件判断
              SinglePicture({ item: item }) // 渲染单图片列表项
            } else {
              MultiPicture({ item: item }) // 渲染多图片列表项
            }
          }
        }, (item: number) => item + '')
      }
    }
  }
}

// 复用组件1


struct SinglePicture {
  // ...
}

// 复用组件2


struct MultiPicture {
  // ...
}
  • 场景B:类型1和类型2布局不同,但是很多业务逻辑公用。可以给同一个组件设置不同的reuseId来进行复用。因为在这种情况下,如果将组件分为两个自定义组件进行复用,会存在代码冗余问题。根据布局的差异,可以给同一个组件设置不同的reuseId从而复用同一个组件,达到逻辑代码的复用。复用组件是依据reuseId来区分复用缓存池的,而自定义组件的名称就是默认的reuseId。因此,可以为复用组件显式设置两个不同的reuseId使用两个自定义组件进行复用相比,对于 ArkUI 而言,复用逻辑完全相同,复用池也一样
    示例代码如下:


struct ReuseType2B {
  // ...

  build() {
    Column() {
      List() {
        LazyForEach(this.dataSource, (item: MemoInfo) => {
          ListItem() {
            MemoItem({ memoItem: item })// 使用reuseId进行组件复用的控制
              .reuseId((item.imageSrc !== '') ? 'withImage' : 'noImage')
          }
        }, (item: MemoInfo) => JSON.stringify(item))
      }
    }
  }
}



export default struct MemoItem {
   memoItem: MemoInfo = MEMO_DATA[0];

  aboutToReuse(params: Record<string, Object>) {
    this.memoItem = params.memoItem as MemoInfo;
  }

  build() {
    Row() {
      // ...
      if (this.memoItem.imageSrc !== '') {
        Image($r(this.memoItem.imageSrc))
          .width(90)
          .aspectRatio(1)
          .borderRadius(10)
      }
    }
    // ...
  }
}

2.3 组合型

该场景为复用组件之间布局有不同,情况非常多,但是拥有共同的子组件。其复用原理是将复用组件改为@Builder,让内部子组件相互之间复用,使复用组件内部共同的子组件的缓存池在父组件上共享。 这个思路其实是本篇博文1.3小节的思路。
在这里插入图片描述
典型场景如下图FriendsMomentsPage,这个列表的Item有多种组合方式。但是每个Item上面和下面的布局是一样的,中间部分的布局有所不同,有单一图片、视频、九宫等等。
在这里插入图片描述
示例代码如下,列举了单一图片、视频和九宫格图片三种类型的列表项目,使用Builder函数后将子组件组合成三种不同的类型,使内部共同的子组件就处于同一个父组件FriendsMomentsPage下。对这些子组件使用组件复用时,他们的缓存池也会在父组件上共享,节省组件创建时的消耗。



struct ReuseType3 {
  // ...

  
  itemBuilderSingleImage(item: FriendMoment) { // 单大图列表项
    // ...
  }

  
  itemBuilderGrid(item: FriendMoment) { // 九宫格列表项
    // ...
  }

  
  itemBuilderVideo(item: FriendMoment) { // 视频列表项
    // ...
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.momentDataSource, (item: FriendMoment) => {
          ListItem() {
            if (item.type === 1) { // 根据不同类型,使用不同的组合
              this.itemBuilderSingleImage(item);
            } else if (item.type === 2) {
              this.itemBuilderGrid(item);
            } else if (item.type === 3) {
              this.itemBuilderVideo(item);
            } else {
              // ...
            }
          }
        }, (moment: FriendMoment) => JSON.stringify(moment))
      }
    }
  }
}



struct ItemTop {
  // ...
}



struct ItemBottom {
  // ...
}



struct MiddleSingleImage {
  // ...
}



struct MiddleGrid {
  // ...
}



struct MiddleVideo {
  // ...
}

另外还有全局型(该类型在写BuildNode的时候会另外说明)、嵌套型的场景,限于篇幅本文不在说明,详见参考资料章节。

3、参考资料

组件复用原理与使用
组件复用场景与方法详解

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐