概述

在滑动场景下,常常会对同一类自定义组件的实例进行频繁的创建与销毁。此时可以考虑通过组件复用减少频繁创建与销毁的能耗。组件复用时,可能存在许多影响组件复用效率的操作,本篇文章将重点介绍如何通过组件复用四板斧提升复用性能。

组件复用四板斧:

  • 第一板斧,减少组件复用的嵌套层级,如果在复用的自定义组件中再嵌套自定义组件,会存在节点构造的开销,且需要在每个嵌套的子组件中的aboutToReuse方法中实现数据的刷新,造成耗时。
  • 第二板斧,优化状态管理,精准控制组件刷新范围,在复用的场景下,需要控制状态变量的刷新范围,避免扩大刷新范围,降低组件复用的效率。
  • 第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成,如:使用if else结构来控制组件的创建,会造成组件树结构的大幅变动,降低组件复用的效率。需使用reuseId标记不同的组件结构,提升复用性能。
  • 第四板斧,不要使用函数/方法作为复用组件的入参,复用时会触发组件的构造,如果函数入参中存在耗时操作,会影响复用性能。

组件复用原理机制

  1. 如上图①中,ListItem N-1滑出可视区域即将销毁时,如果标记了@Reusable,就会进入这个自定义组件所在父组件的复用缓存区。需注意在自定义组件首次显示时,不会触发组件复用。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其是该复用组件具有相同的布局结构,仅有某些数据差异时,通过组件复用可以提高列表页面的加载速度和响应速度。

  2. 如上图②中,复用缓存池是一个Map套Array的数据结构,以reuseId为key,具有相同reuseId的组件在同一个Array中。如未设置reuseId,则reuseId默认是自定义组件的名字。

  3. 如上图③中,发生复用行为时,会自动递归调用复用池中取出的自定义组件的aboutToReuse回调,应用可以在这个时候刷新数据。 ​

第一板斧,减少组件复用的嵌套层级

在组件复用场景下,过深的自定义组件的嵌套会增加组件复用的使用难度,比如需要逐个实现所有嵌套组件中aboutToReuse回调实现数据更新;因此推荐优先使用@Builder替代自定义组件,减少嵌套层级,利于维护切能提升页面加载速度。正反例如下:

反例:

@Entry
@Component
struct ReduceLevel {private data: BasicDateSource = new BasicDateSource();

 ​aboutToAppear(): void {for (let index = 0; index < 30; index++) {this.data.pushData(index.toString())}}

 ​build() {
   ​Column() {
     ​List() {
       ​LazyForEach(this.data, (item: string) => {
         ​ListItem() {//反例 使用自定义组件
           ​ComponentA({ desc: item })}}, (item: string) => item)}}}
}

@Reusable
@Component
struct ComponentA {@State desc: string = '';

 ​aboutToReuse(params: ESObject): void {this.desc = params.desc as string;}

 ​build() {// 在复用组件中嵌套使用自定义组件
   ​ComponentB({ desc: this.desc })}
}


@Component
struct ComponentB {@State desc: string = '';// 嵌套的组件中也需要实现aboutToReuse来进行UI的刷新
 ​aboutToReuse(params: ESObject): void {this.desc = params.desc as string;}

 ​build() {
   ​Column() {
     ​Text('子组件' + this.desc).fontSize(30).fontWeight(30)}}
}

上述反例的操作中,在复用的自定义组件中嵌套了新的自定义组件。ArkUI中使用自定义组件时,在build阶段将在在后端FrameNode树创建一个相应的CustomNode节点,在渲染阶段时也会创建对应的RenderNode节点。会造成组件复用下,CustomNode创建和和RenderNod渲染e的耗时。且嵌套的自定义组件ComponentB,也需要实现aboutToReuse来进行数据的刷新。

正例:

@Entry
@Component
struct ReduceLevel {private data: BasicDateSource = new BasicDateSource();

 ​aboutToAppear(): void {for (let index = 0; index < 30; index++) {this.data.pushData(index.toString())}}

 ​build() {
   ​Column() {
     ​List() {
       ​LazyForEach(this.data, (item: string) => {
         ​ListItem() {//  正例
           ​ChildComponent({ desc: item })}}, (item: string) => item)}}}
}

// 正例 使用组件复用
@Reusable
@Component
struct ChildComponent {@State desc: string = '';

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

 ​build() {
   ​Column() {// 使用@Builder,可以减少自定义组件创建和渲染的耗时
     ​ChildComponentBuilder({ paramA: this.desc })}}
}

class Temp {
 ​paramA: string = '';
}

@Builder
function ChildComponentBuilder($$: Temp) {
 ​Column() {// 此处使用`${}`来进行按引用传递,让@Builder感知到数据变化,进行UI刷新
   ​Text(子组件 + ${$$.paramA}).fontSize(30).fontWeight(30)}
}

上述正例的操作中,在复用的自定义组件中用@Builder来代替了自定义组件。避免了CustomNode节点创建和RenderNode渲染的耗时。

第二板斧,优化状态管理,精准控制组件刷新范围使用

1.使用attributeModifier精准控制组件属性的刷新,避免组件不必要的属性刷新

复用场景常用在高频的刷新场景,精准控制组件的刷新范围可以有效减少主线程渲染负载,提升滑动性能。正反例如下:

反例:

@Entry
@Component
struct PreciseRefreshing {@State mainContentData: VideoDataSource = new VideoDataSource(); // 视频展示列表

 ​build() {
   ​Column() {
     ​List() {
       ​LazyForEach(this.mainContentData, (item: VideoDataType) => {
         ​ListItem() {
           ​MyComponent({ authorName: item.authorName, fontSize: item.fontWeight })}}, (item: VideoDataType) => item.desc + item.fontWeight)}}}
}

@Reusable
@Component
export struct MyComponent {...@State fontSize: number = 0;

 ​aboutToReuse(params: ESObject): void {this.authorName = params.authorName;this.fontSize = params.fontSize;}

 ​build() {
   ​RelativeContainer() {
     ​Text(this.videoDesc).textAlign(TextAlign.Center).fontStyle(FontStyle.Normal).fontColor(Color.Pink).id('videoName').margin({ left: 10 }).fontWeight(30).alignRules({'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },'left': { 'anchor': 'image', 'align': HorizontalAlign.End }})// 此处使用属性直接进行刷新,会造成Text所有属性都刷新.fontSize(this.fontSize)}.width('100%').height(100)}
}

上述反例的操作中,通过aboutToReuse对fontSize状态变量更新,进而导致组件的全部属性进行刷新,造成不必要的耗时。可以考虑对需要更新的组件的属性,进行精准刷新,避免不必要的重绘和渲染。

正例:

export class MyTextModifier implements AttributeModifier<TextAttribute> {private fontSize: number = 30;

 ​constructor() {}

 ​setFontSize(instance: TextAttribute,fontSize: number) {
   ​instance.fontSize = fontSize;return this;}

 ​applyNormalAttribute(instance: TextAttribute): void {
   ​instance.textAlign(TextAlign.Center)
   ​instance.fontStyle(FontStyle.Normal)
   ​instance.fontColor(Color.Pink)
   ​instance.id('videoName')
   ​instance.margin({ left: 10 })
   ​instance.fontWeight(30)
   ​instance.fontSize(10)
   ​instance.alignRules({'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },'left': { 'anchor': 'image', 'align': HorizontalAlign.End }})}
}

@Entry
@Component
struct PreciseRefreshing {@State mainContentData: VideoDataSource = new VideoDataSource(); // 视频展示列表


 ​build() {
   ​Column() {
     ​List() {
       ​LazyForEach(this.mainContentData, (item: VideoDataType) => {
         ​ListItem() {
           ​MyComponent({... fontSize: item.fontWeight })}}, (item: VideoDataType) => item.desc + item.fontWeight)}}}
}


@Reusable
@Component
export struct MyComponent {...@State fontSize: number = 0;
 ​textModifier:MyTextModifier=new MyTextModifier();

 ​aboutToReuse(params: ESObject): void {...this.fontSize = params.fontSize;this.textModifier.setFontSize(this.textModifier,this.fontSize)}

 ​build() {
   ​RelativeContainer() {...
     ​Text(this.videoDesc)// 采用attributeModifier来对需要更新的fontSize属性进行精准刷新,避免不必要的属性刷新。.attributeModifier(this.textModifier)...}}
}

上述正例的操作中,通过attributeModifier属性来对text组件需要刷新的fontSize属性进行精准刷新,避免text其它不需要更改的属性的刷新。

2.使用@Link/@ObjectLink替代@Prop减少深拷贝,提升组件创建速度

在父子组件数据同步时,如果仅仅是需要父组件向子组件同步数据,不存在修改子组件的数据变化不同步给父组件的需求。建议使用@Link/@ObjectLink替代@Prop,@Prop在装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。正反例如下:

反例:

@Component
struct ChildComponent {@Prop message: string;

 ​build() {
   ​Column() {
     ​Text(this.message).fontSize(50).fontWeight(FontWeight.Bold)}}
}

@Entry
@Component
struct FatherComponent {@State message: string = 'Hello World';

 ​build() {
   ​Column() {
     ​ChildComponent({ message: this.message })}}
}

上述反例的操作中,父子组件之间的数据同步用了@Prop来进行,每个@Prop装饰的变量在初始化时都在本地拷贝了一份数据。会增加创建时间及内存的消耗,造成性能问题。

正例:

@Component
struct ChildComponent {@Link message: string;

 ​build() {
   ​Column() {
     ​Text(this.message).fontSize(50).fontWeight(FontWeight.Bold)}}
}


@Entry
@Component
struct FatherComponent {@State message: string = 'Hello World';

 ​build() {
   ​Column() {
     ​ChildComponent({ message: this.message })}.width('100%').height('100%')}
}

上述正例的操作中,父子组件之间的数据同步用了@Link来进行,子组件@Link包装类把当前this指针注册给父组件,会直接将父组件的数据同步给子组件,实现父子组件数据的双向同步,降低子组件创建时间和内存消耗。

第三板斧,复用组件嵌套结构会变更的场景,使用reuseId标记不同结构的组件构成

在自定义组件复用的场景中,如果使用if/else条件语句来控制布局的结构,会导致在不同逻辑创建不同布局结构嵌套的组件,从而造成组件树结构的不同。此时我们应该使用reuseId来区分不同结构的组件,确保系统能够根据reuseId缓存各种结构的组件,提升复用性能。正反例如下:

反例:

@Entry
@Component
struct ReuseID {...
 ​build() {
   ​Column() {
     ​List({ scroller: this.scroller }) {
       ​LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
         ​ListItem() {
           ​Button({ type: ButtonType.Normal }) {
             ​Row() {if (chatInfo['isPublicChat']) {
                 ​PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })} else {
                 ​ChatItem({ chatInfo: chatInfo as ChatSessionEntity }).onClick(() => {const sessionType = (chatInfo as ChatSessionEntity).sessionType
                     ​autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
                     ​imLogic.chat.chatSort()})}}.padding({ left: 16, right: 16 })}.type(ButtonType.Normal).width('100%').height('100%').backgroundColor('#fff').borderRadius(0)}.height(72).swipeAction({
           ​end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))})}, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority))}.cachedCount(3).backgroundColor('#fff').onScrollIndex(startIndex => {this.listStartIndex = startIndex;}).width('100%').height('100%')}}
}
@Reusable
@Component
struct PublicChatItem {...
 ​aboutToReuse(params: ESObject): void {this.chatInfo = params.chatInfo
 ​}
 ​build() {...}
}@Reusable
@Component
struct ChatItem {
 ​aboutToReuse(params: ESObject): void {this.chatInfo = params.chatInfo
 ​}
 ​build() {...}
}

上述反例的操作中,通过if else来控制组件树走不同的分支,分别复用PublicChatItem组件和ChatItem组件。导致更新if分支时仍然走删除重创的逻辑。考虑采用根据不同的分支设置不同的reuseId来提高复用的性能。

正例:

@Entry
@Component
struct ReuseID {...
 ​build() {
   ​Column() {
     ​List({ scroller: this.scroller }) {
       ​LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
         ​ListItem() {// 使用reuseId进行组件复用的控制
           ​InnerRecentChat({ chatInfo: chatInfo }).reuseId(this.lazyChatList.getReuseIdByIndex(index))}.height(72).swipeAction({
           ​end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))})}, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority))}.cachedCount(3).backgroundColor('#fff').onScrollIndex(startIndex => {this.listStartIndex = startIndex;}).width('100%').height('100%')}}
}

@Reusable
@Component
struct InnerRecentChat {...
 ​aboutToReuse(params: ESObject): void {this.chatInfo = params.chatInfo
 ​}

 ​build() {
   ​Button({ type: ButtonType.Normal }) {
     ​Row() {if (this.chatInfo['isPublicChat']) {
         ​PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })} else {
         ​ChatItem({ chatInfo: chatInfo as ChatSessionEntity }).onClick(() => {const sessionType = (chatInfo as ChatSessionEntity).sessionType
             ​autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
             ​imLogic.chat.chatSort()})}}.padding({ left: 16, right: 16 })}.type(ButtonType.Normal).width('100%').height('100%').backgroundColor('#fff').borderRadius(0)}
}

class MtDataSource extends BasicDataSource{private chatList:Array<ChatSessionEntity|IChat.PublicChat>=[];private reuseIds:Array<string>=[];public totalCount():number{return this.chatList.length;}public set (list:Array<ChatSessionEntity|IChat.PublicChat>){this.chatList=list;this.reuseIds=list.map((value:ChatSessionEntity|IChat.PublicChat)=>{if (value['isPublicChat']) {return "public";}else {if ((value as ChatSessionEntity).target?.isEmployeeEntity()) {return "employee"}else {return "group"}}})this.notifyDataReload();}
   ​pubilc getReuseIdByIndex(index:number):string{return this.reuseIds
   ​}
}

上述正例的操作中,通过reuseId来标识需要复用的组件,省去走if else删除重创的逻辑,提高组件复用的效率和性能。

第四板斧,避免使用函数/方法作为复用组件创建时的入参

由于在组件复用的场景下,每次复用都需要重新创建组件关联的数据对象,导致重复执行入参中的函数来获取入参结果。如果函数中存在耗时操作,会严重影响性能。正反例如下:

【反例】

// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导
// 此处为复用的自定义组件
@Reusable
@Component
struct ChildComponent {@State desc: string = '';@State sum: number = 0;

 ​aboutToReuse(params: Record<string, Object>): void {this.desc = params.desc as string;this.sum = params.sum as number;}

 ​build() {
   ​Column() {
     ​Text('子组件' + this.desc).fontSize(30).fontWeight(30)
     ​Text('结果' + this.sum).fontSize(30).fontWeight(30)}}
}

@Entry
@Component
struct Reuse {private data: BasicDateSource = new BasicDateSource();

 ​aboutToAppear(): void {for (let index = 0; index < 20; index++) {this.data.pushData(index.toString())}}
   ​
 ​// 真实场景的函数中可能存在未知的耗时操作逻辑,此处用循环函数模拟耗时操作
 ​count(): number {let temp: number = 0;for (let index = 0; index < 10000; index++) {
     ​temp += index;}return temp;}

 ​build() {
   ​Column() {
     ​List() {
       ​LazyForEach(this.data, (item: string) => {
         ​ListItem() {// 此处sum参数是函数获取的,实际开发场景无法预料该函数可能出现的耗时操作,每次进行组件复用都会重复触发此函数的调用
           ​ChildComponent({ desc: item, sum: this.count() })}.width('100%').height(100)}, (item: string) => item)}}}
}

上述反例的操作中,复用的子组件参数sum是通过耗时函数生成。该函数在每次组件复用时都需要执行,会造成性能问题,甚至是列表滑动过程中的卡顿丢帧现象。

【正例】

// 下文中BasicDateSource是实现IDataSource接口的类,具体可参考LazyForEach用法指导
// 此处为复用的自定义组件
@Reusable
@Component
struct ChildComponent {@State desc: string = '';@State sum: number = 0;

 ​aboutToReuse(params: Record<string, Object>): void {this.desc = params.desc as string;this.sum = params.sum as number;}

 ​build() {
   ​Column() {
     ​Text('子组件' + this.desc).fontSize(30).fontWeight(30)
     ​Text('结果' + this.sum).fontSize(30).fontWeight(30)}}
}

@Entry
@Component
struct Reuse {private data: BasicDateSource = new BasicDateSource();@State sum: number = 0;

 ​aboutToAppear(): void {for (let index = 0; index < 20; index++) {this.data.pushData(index.toString())}// 执行该异步函数this.count();}// 模拟耗时操作逻辑async count() {let temp: number = 0;for (let index = 0; index < 10000; index++) {
     ​temp += index;}// 将结果放入状态变量中this.sum = temp;}

 ​build() {
   ​Column() {
     ​List() {
       ​LazyForEach(this.data, (item: string) => {
         ​ListItem() {// 子组件的传参通过状态变量进行
           ​ChildComponent({ desc: item, sum: this.sum })}.width('100%').height(100)}, (item: string) => item)}}}
}

上述正例的操作中,通过耗时函数count生成的结果不变,可以将其放到页面初始渲染时执行一次,将结果赋值给this.sum。在复用组件的参数传递时,通过this.sum来进行。

码牛课堂也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线。大家可以进行参考学习:https://qr21.cn/FV7h05

①全方位,更合理的学习路径
路线图包括ArkTS基础语法、鸿蒙应用APP开发、鸿蒙能力集APP开发、次开发多端部署开发、物联网开发等九大模块,六大实战项目贯穿始终,由浅入深,层层递进,深入理解鸿蒙开发原理!

②多层次,更多的鸿蒙原生应用
路线图将包含完全基于鸿蒙内核开发的应用,比如一次开发多端部署、自由流转、元服务、端云一体化等,多方位的学习内容让学生能够高效掌握鸿蒙开发,少走弯路,真正理解并应用鸿蒙的核心技术和理念。

③实战化,更贴合企业需求的技术点
学习路线图中的每一个技术点都能够紧贴企业需求,经过多次真实实践,每一个知识点、每一个项目,都是码牛课堂鸿蒙研发团队精心打磨和深度解析的成果,注重对学生的细致教学,每一步都确保学生能够真正理解和掌握。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

鸿蒙开发学习手册》:https://qr21.cn/FV7h05

如何快速入门:

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05

大厂鸿蒙面试题::https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

Logo

由 GitCode & CSDN 共同运营的 HarmonyOS 开发者社区,服务鸿蒙开发者,帮助开发者快速了解鸿蒙、学习鸿蒙、获取最新最全的鸿蒙开发者资料与体系课程,共同赋能鸿蒙生态。

更多推荐