概述

组件复用是优化用户界面性能,提升应用流畅度的一种重要手段,通过复用已存在的组件节点而非创建新的节点,从而确保UI线程的流畅性与响应速度。

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

然而,面对复杂的业务场景或者布局嵌套的场景下,组件复用使用不当,可能会导致复用失效或者性能提升不能最大化。例如列表中存在多种布局形态的列表项,无法直接复用。

本文基于对常见的布局类型进行划分,通过合理使用组件复用方式,帮助开发者更好的理解和实施组件复用策略以优化应用性能。

组件复用实现方法

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

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

根据这个原理系统提供的组件复用有如下两条约束和限制:

  • 不同复用组件(组件名不同或者reuseld不同)之间相同子组件无法复用,因为它们在缓存池的不同List中,如下图所示,子组件B之间无法复用。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    如果想要子组件B之间进行复用,可以将复用组件改为@Builder函数,然后将子组件B使用@Reusable修饰,这样子组件的缓存池就会在父组件上共享

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 不同父组件中的相同子组件无法复用,因为它们在不同的缓存池,如下图所示,复用组件1之间无法复用。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    对于这种嵌套比较多的情况下,导致想要复用的组件不在一个父组件的场景下,可以使用BuilderNode实现全局复用。

复用类型详解

组件复用基于不同的布局效果和复用的诉求,可以分为以下五种类型。

复用类型 描述 复用思路
[标准型] 复用组件之间布局完全相同 标准复用
[有限变化型] 复用组件之间布局有所不同,但是类型有限 使用reuseId或者独立成不同自定义组件
[组合型] 复用组件之间布局有不同,情况非常多,但是拥有共同的子组件 将复用组件改为@Builder,让内部子组件相互之间复用
[全局型] 组件可在不同的父组件中复用,并且不适合使用@Builder 使用BuilderNode自定义复用组件池,在整个应用中自由流转
[嵌套型] 复用组件的子组件的子组件存在差异 采用化归思想将嵌套问题转化为上面四种标准类型来解决

下面将以滑动列表的场景为例介绍5种复用类型的使用场景,为了方便描述,下文将需要复用的自定义组件如ListItem的内容组件,叫做复用组件,将其下层的自定义组件叫做子组件、复用组件上层的自定义组件叫做父组件。为了更直观,下面每一种复用类型都会通过简易的图形展示组件的布局方式,并且为了便于分辨,布局相同的子组件使用同一种形状图形表示。

标准型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是一个标准的组件复用场景,一个滚动容器内的复用组件布局相同,只有数据不同,这种类型的组件复用可以直接参考资料[组件复用]。其缓存池如下,因为该场景只有一个复用组件,所以在缓存中只有一个复用组件list:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

典型场景如下,列表Item布局基本完全相同。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

标准型组件复用的示例代码如下:

@Entry
@Component
struct ReuseType1 {
  // ...
  build() {
    Column() {
      List() {
        LazyForEach(this.dataSource, (item: string) => {
          ListItem() {
            CardView({ item: item })
          }
        }, (item: string) => item)
      }
    }
  }
}

// 复用组件
@Reusable
@Component
export struct CardView {
  @State item: string = '';

  aboutToReuse(params: Record<string, Object>): void {
    this.item = params.item as string;
  }
  // ...
}

有限变化型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如上图所示,有限变化型指的是父组件内存在多个类型的复用单元,这些类型的单元布局有所不同,根据业务逻辑的差异可以分为以下两种情况:

  • 类型1和类型2布局不同,业务逻辑不同: 这种情况可以使用两个不同的自定义组件进行复用。
  • 类型1和类型2布局不同,但是很多业务逻辑公用: 这种情况为了复用公用的逻辑代码,减少代码冗余,可以给同一个组件设置不同的reuseId来进行复用。

下面将分别介绍这两种场景下的组件复用方法。

类型1和类型2布局不同,业务逻辑不同:因为两种类型的组件布局会对应应用不同的业务处理逻辑,建议将两种类型的组件分别使用两个不同的自定义组件,分别进行复用。给复用组件1和复用组件2设置不同的reuseId,此时组件复用池内的状态如下图所示,复用组件1和复用组件2处于不同的复用list中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例如下面的列表场景,列表项布局差距比较大,有多图片的列表项,有单图片的列表项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现方式可参考以下示例代码:

@Entry
@Component
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
@Reusable
@Component
struct SinglePicture {
  // ...
}

// 复用组件2
@Reusable
@Component
struct MultiPicture {
  // ...
}

类型1和类型2布局不同, 但是很多业务逻辑公用:在这种情况下,如果将组件分为两个自定义组件进行复用,会存在代码冗余问题。根据布局的差异,可以给同一个组件设置不同的reuseId从而复用同一个组件,达到逻辑代码的复用。

根据[组件复用原理与使用]可知,复用组件是依据reuseId来区分复用缓存池的,而自定义组件的名称就是默认的reuseId。因此,为复用组件显式设置两个不同的reuseId与使用两个自定义组件进行复用,对于 ArkUI 而言,复用逻辑完全相同,复用池也一样,只不过复用池中复用组件的list以reuseId作为标识, 如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例如下面这个场景,布局差异比较小,业务逻辑一样都是跳转到页面详情。这种情况复用同一个组件,只需要使用if/else条件语句来控制布局的结构,就可以实现,同时可以复用跳转详情的公用逻辑代码。但是这样会导致在不同逻辑会反复去修改布局,造成性能损耗。开发者可以根据不同的条件,设置不同的reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现方式可以参考以下示例:

@Entry
@Component
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))
      }
    }
  }
}

@Reusable
@Component
export default struct MemoItem {
  @State 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)
      }
    }
    // ...
  }
}

组合型

这种类型中复用组件之间存在不同,并且情况比较多,但拥有共同的子组件。如果使用有限变化型的组件复用方式,将所有类型的复用组件写成自定义组件分别复用,不同复用组件(组件名不同或者reuseld不同)之间相同子组件无法复用,因为它们在缓存池的不同List中。

对此可以将复用组件转变为@Builder函数,使复用组件内部共同的子组件的缓存池在父组件上共享,此时组件复用池内的状态如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

典型场景如下图,这个列表的Item有多种组合方式。但是每个Item上面和下面的布局是一样的,中间部分的布局有所不同,有单一图片、视频、九宫等等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例代码如下,列举了单一图片、视频和九宫格图片三种类型的列表项目,使用Builder函数后将子组件组合成三种不同的类型,使内部共同的子组件就处于同一个父组件FriendsMomentsPage下。对这些子组件使用组件复用时,他们的缓存池也会在父组件上共享,节省组件创建时的消耗。

@Entry
@Component
struct ReuseType3 {
  // ...

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

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

  @Builder
  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))
      }
    }
  }
}

@Reusable
@Component
struct ItemTop {
  // ...
}

@Reusable
@Component
struct ItemBottom {
  // ...
}

@Reusable
@Component
struct MiddleSingleImage {
  // ...
}

@Reusable
@Component
struct MiddleGrid {
  // ...
}

@Reusable
@Component
struct MiddleVideo {
  // ...
}

全局型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

默认的组件复用行为,是将子组件放在父组件的缓存池里,受到这个限制,不同父组件中的相同子组件无法复用,推荐的解决方案是将父组件改为builder函数,让子组件共享组件复用池,但是由于在一些应用场景下,父组件承载了复杂的带状态的业务逻辑,而builder是无状态的,修改会导致难以维护,因此开发者可以使用BuilderNode自行管理组件复用池。

如下图所示,有时候应用在多个tab页之间切换,tab页之间结构类似,需要在tab页之间复用组件,提升页面切换性能。或者有些应用在组合型场景下,由于复用组件内部含有较多带状态的业务逻辑,所以不适合改为Builder函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

针对这种类型的组件复用场景,可以通过[BuilderNode]自定义缓存池,将要复用的组件封装在BuilderNode中,将BuilderNode的NodeController作为复用的最小单元,自行管理复用池。

嵌套型

图1 嵌套型示意图
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

嵌套型是指复用组件的子组件的子组件之间存在差异的复用场景。如上图所示,列表项复用组件1之间的差异是子组件B的子组件不一样,有子组件C、D、E三种。这种情况可以运行化归的思想,将复杂的问题转化为已知的、简单的问题

嵌套型实际上是上面四种类型的组合,以上图为例,可以通过[有限变化型]的方案,将子组件B变为子组件B1/B2/B3,这样问题就变成了一个标准的有限变化型,A/B1/C、A/B2/D、A/B3/E会分别作为一个组合进行复用,复用池如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面列举一个简单的示例介绍嵌套型的使用:

@Entry
@Component
struct ReuseType5A {
  // ...
  build() {
    Column() {
      List() {
        LazyForEach(this.dataSource, (item: number) => {
          ListItem() {
            if (item % 2 === 0) { // 模拟类型一的条件
              ReusableComponent({ item: item })
                .reuseId('type1')
            } else if (item % 3 === 0) { // 模拟类型二的条件
              ReusableComponent({ item: item })
                .reuseId('type2')
            } else { // 模拟类型三的条件
              ReusableComponent({ item: item })
                .reuseId('type3')
            }
          }
        }, (item: number) => item.toString())
      }
    }
  }
}

// 复用组件
@Reusable
@Component
struct ReusableComponent {
  @State item: number = 0;

  build() {
    Column() {
      ComponentA()
      if (this.item % 2 === 0) {
        ComponentB1()
      } else if (this.item % 3 === 0) {
        ComponentB2()
      } else {
        ComponentB3()
      }
    }
  }
}

@Component
struct ComponentA {
  // ...
}

@Component
struct ComponentB1 {
  build() {
    Column() {
      ComponentC()
    }
  }
}

@Component
struct ComponentB2 {
  build() {
    Column() {
      ComponentD()
    }
  }
}

@Component
struct ComponentB3 {
  build() {
    Column() {
      ComponentE()
    }
  }
}

@Component
struct ComponentC {
  // ...
}

@Component
struct ComponentD {
  // ...
}

@Component
struct ComponentE {
  // ...
}

或者通过组合型的方案,将子组件B改为@Builder函数,使子组件C、D、E和子组件A在同一组件层级。然后分别给子组件A、C、D、E添加@Reusable装饰器,使他们能够单独复用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面还是以朋友圈为例,如果复用组件的嵌套比较复杂,复用组件内部含有带状态的业务逻辑,不适合改为Builder函数,这种情况可能就不适合使用组合型了,这时候可以尝试通过有限变化型来复用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图中列表项的复用组件为CardItem,子组件由三部分组成ItemTop、ItemMiddle以及ItemBottom,其中ItemMiddle子组件(即复用组件CardItem子组件的子组件)之间有差异,对于这种情况的复用,可以将子ItemMiddle组件变为ItemMiddle1、ItemMiddle2、ItemMiddle3,这样就变成了一个有限变化型复用。示例代码如下:

@Entry
@Component
struct ReuseType5 {
  // ...
  build() {
    Column() {
      List({ space: 30 }) {
        LazyForEach(this.momentDataSource, (item: FriendMoment, index: number) => {
          ListItem() {
            Column() {
              if (item.type === 1) {
                CardItem({ item: item })
                  .reuseId('ItemMiddle1')
              } else if (item.type === 2) {
                CardItem({ item: item })
                  .reuseId('ItemMiddle2')
              } else if (item.type === 3) {
                CardItem({ item: item })
                  .reuseId('ItemMiddle3')
              } else {
                // ...
              }
            }
          }
        }, (item: FriendMoment) => JSON.stringify(item))
      }
    }
  }
}

@Reusable
@Component
struct CardItem {
  // ...
  build() {
    Column() {
      ItemTop({ item: this.item })
      if (this.item.type === 1) { // 模拟条件1
        ItemMiddle1({ item: this.item })
      } else if (this.item.type === 2) { // 模拟条件2
        ItemMiddle2({ item: this.item })
      } else if (this.item.type === 3) {
        ItemMiddle3({ item: this.item }) // // 模拟条件3
      } else {
        // ...
      }
      ItemBottom({ item: this.item })
    }
  }
}

@Component
struct ItemTop {
  // ...
}

@Component
struct ItemBottom {
  // ...
}

@Component
struct ItemMiddle1 {
  // ...
  build() {
    Column() {
      CommentText()
      MiddleSingleImage({ item: this.item })
    }
  }
}

@Component
struct ItemMiddle2 {
  // ...
  build() {
    Column() {
      CommentText()
      MiddleGrid({ item: this.item })
    }
  }
}

@Component
struct ItemMiddle3 {
  // ...
  build() {
    Column() {
      CommentText()
      MiddleVideo({ item: this.item })
    }
  }
}

@Component
struct MiddleSingleImage {
  // ...
}

@Component
struct MiddleGrid {
  // ...
}

@Component
struct MiddleVideo {
  // ...
}

@Component
struct CommentText {
  // ...
}

总结

组件复用需要根据具体的业务场景,选择合理的复用方案,才能更好的优化应用性能。

Logo

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

更多推荐