OpenHarmony实战开发-组件复用四板斧。

567 篇文章 0 订阅
555 篇文章 0 订阅

概述

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

组件复用四板斧:

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

组件复用原理机制

在这里插入图片描述

  1. 如上图①中,ListItemN-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来进行。

如果大家还没有掌握鸿蒙,现在想要在最短的时间里吃透它,我这边特意整理了《鸿蒙语法ArkTS、TypeScript、ArkUI等…视频教程》以及《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

鸿蒙语法ArkTS、TypeScript、ArkUI等…视频教程:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

在这里插入图片描述

OpenHarmony APP应用开发教程步骤:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

在这里插入图片描述

《鸿蒙开发学习手册》:

如何快速入门:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

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

在这里插入图片描述

开发基础知识:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

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

在这里插入图片描述

基于ArkTS 开发:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

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

在这里插入图片描述

鸿蒙生态应用开发白皮书V2.0PDF:https://docs.qq.com/doc/DZVVBYlhuRkZQZlB3

在这里插入图片描述

  • 20
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微信小程序是一种基于微信平台的应用的开发模式,可以快速的开发出符合用户需求的小程序。在小程序的开发中,组件是一个非常重要的概念,通过组件可以实现复用性和模块化编程思想。 组件应用是小程序开发的基础。通过组件可以将某一模块化并封装起来,使得组件可以在不同的页面间得到复用,大大提升了开发效率并减少了代码冗余。微信小程序提供了丰富的自带组件,包括文本、图片、按钮、输入框等等,开发者也可以自己开发组件来满足自己的需求。实际开发中,通过组件可以快速搭建页面框架和业务逻辑。 Demo是一个演示小程序的示例程序。在小程序的实际开发过程中,一个好的Demo非常重要。通过Demo,开发人员可以更深入的了解小程序的开发流程、组件的应用和实际的业务开发等等。在Demo中,通常会包括小程序的一些基础操作,如页面跳转、数据绑定、组件的使用等。而在实际开发中,Demo还会包括一些复杂的业务场景,如支付、登录、数据列表展示等等。Demo不仅为开发者提供了学习和实践的机会,也方便了使用者了解该小程序的功能和特点。 总之,微信小程序组件的应用和Demo的开发都是小程序开发过程中非常重要的两个部分。良好的组件应用和精心设计的Demo,可以在极短的时间内实现小程序开发

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值