鸿蒙HarmonyOS开发实战( Beta5版)全局自定义组件复用实现

133 篇文章 0 订阅
133 篇文章 0 订阅

简介

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

实现思路

  1. 将要生成自定义组件地方用NodeContainer占位,将NodeContainer内部的NodeController按照组件类型分别存储在NodePool中。
  2. 每次需要创建子组件时,优先从NodePool中取出一个组件,如果NodePool中没有可复用的组件则重新创建一个,否则就更新一下数据。当NodeController销毁时,回收到NodePool中,供下次使用。

数据结构

image-20240531161153519

创建和销毁过程中复用池的流程

image-20240531164427730

应用场景

在应用开发中,会遇到需要页面切换的场景,比如某些视频APP的首页,就是一个List(标题)+Swiper(列表页面)实现的Tabs切换场景(如图1所示)。Swiper中每个页面都使用瀑布流加载视频列表,各个瀑布流中的子组件有可能是相同的布局,为了提升应用性能,就会有跨页面复用子组件的需求。但是在ArkUI提供的常规复用中,复用池是放在父组件中的,这就导致跨页面时无法复用上一个页面瀑布流中的子组件。此时就可以使用BuilderNode自定义一个全局的组件复用池,根据页面状态创建、回收、复用子组件,实现组件的跨页面复用。

图1 应用场景示意图

custom_reusable_pool

示例代码

下面通过常规复用和自定义组件复用池两种方式,对比组件复用的性能。

常规复用

  1. 使用List+Swiper实现Tabs页面切换。

    // ...
    List() {
      ForEach(this.arrayTitle, (title: Title, index: number) => {
        ListItem() {
          TitleView({
            title: title, clickListener: () => {
              if (title.isSelected) {
                return;
              }
              this.swiperController.changeIndex(index, true);
              this.arrayTitle[index].isSelected = true;
              this.arrayTitle[this.selectIndex].isSelected = false;
              this.selectIndex = index;
            }
          })
          // ...
        }
      })
    }
    .height(30)
    .listDirection(Axis.Horizontal)
    
    Swiper(this.swiperController) {
      // 使用LazyForEach,使Swiper页面按需加载,而不是一次全部创建
      LazyForEach(this.array, () => {
        TabComp()
      }, (title: string) => title)
    }
    .loop(false)
    .onChange((index: number) => { 
      if (this.selectIndex !== index) {
        this.arrayTitle[index].isSelected = true;
        this.arrayTitle[this.selectIndex].isSelected = false;
        this.selectIndex = index;
      }
    })
    .cachedCount(0) // 此处设置cachedCount为0,便于性能对比,实际开发中可按需设置
    // ...
  2. 使用Swiper组件实现轮播图,使用WaterFlow组件实现瀑布流加载数据,并给自定义组件设置reuseId,用于组件复用。

    // ...
    Scroll(this.scroller) {
      Column({ space: 2 }) {
        SwiperBuilder({images: this.images})
    
        WaterFlow() {
          LazyForEach(this.dataSource, (item: ViewItem, index: number) => {
            FlowItem() {
              FlowItemComp({
                item: item,
                itemHeight: this.itemHeightArray[index % 100],
                itemColor: Color.White,
                updater: (item: ViewItem) => {
                  this.fillNewData(item);
                }
              }).reuseId('reuse_type_')
            }
            .width('100%')
          }, (item: string) => item)
        }
        // ...
        .nestedScroll({ // 设置嵌套滑动属性,使
          scrollForward: NestedScrollMode.PARENT_FIRST,
          scrollBackward: NestedScrollMode.SELF_FIRST
        })
      }
    }.width('100%')
    .height('100%')
    // ...
  3. 实现瀑布流的子组件。

    // 需要添加@Reusable装饰器,并实现aboutToReuse接口用于组件复用时刷新数据
    @Reusable
    @Component
    export struct FlowItemComp {
      // ...
    
      build() {
        // ...
      }
      // 通过aboutToReuse接口刷新复用后的数据
      aboutToReuse(params: ESObject): void {
        this.item = params.item;
        this.itemHeight = params.itemHeight;
        this.itemColor = params.itemColor;
      }
    }

编译运行后,点击Tabs切换页面,然后抓取Trace,通过图2可以看到,切换Tabs时,每个页面的首帧创建耗时(从点击Tab的DispatchTouchEvent标签开始,到sendCommands标签通知系统绘制组件结束)都在30-40ms左右。这是因为使用@Reusable的组件复用,是使用了父组件的复用池。FlowItemComp的父组件是WaterFlow,Tab切换时新页面的WaterFlow会被重新创建,这就导致前一个页面的复用池是无法使用的,只能重新创建所有的子组件。

图2 常规复用Trace图

img

自定义组件复用池

  1. 使用List+Swiper实现Tabs页面切换。

    // ...
    Swiper(this.swiperController) {
      LazyForEach(this.array, () => {
        TabNode()
      }, (title: string) => title)
    }
    // ...
  2. 继承NodeController,实现makeNode,用于组件的创建或刷新,并在组件隐藏时(aboutToDisappear)回收组件。

    export class NodeItem extends NodeController {
      private callback: UpdaterCallback | null = null;
      // ...
      // 父类方法,用于创建子组件
      makeNode(uiContext: UIContext): FrameNode | null {
        if (!this.node) {
          this.node = new BuilderNode(uiContext);
          this.node.build(this.builder, this.data);
        } else {
          this.node.update(this.data);
          this.update(this.data);
        }
    
        return this.node.getFrameNode();
      }
      // 组件隐藏时回收组件
      aboutToDisappear(): void {
        NodePool.getInstance().recycleNode(this.type, this);
      }
      // ...
    }
  3. 使用单例模式实现复用池,应用内统一管理组件复用

    export class NodePool {
      private static instance: NodePool;
      // ...
    
      private constructor() {
        this.nodePool = new HashMap();
        this.nodeHook = new HashSet();
        this.idGen = 0;
      }
      // 单例模式,可以全局统一管理
      public static getInstance() {
        if (!NodePool.instance) {
          NodePool.instance = new NodePool();
        }
        return NodePool.instance;
      }
      // ...
    }
  4. 添加getNode方法,根据传入的type参数,获取对应的Node组件,如果未找到,则重新创建

      // ...
      // 获取Node组件,如果存在type类型的Node组件,则直接使用,否则重新创建
      public getNode(type: string, data: ESObject, builder: WrappedBuilder<ESObject>): NodeItem | undefined {
        let node: NodeItem | undefined = this.nodePool.get(type)?.pop();
        if (!node) {
          node = new NodeItem(builder, data, type);
          this.nodeHook.add(node);
        } else {
          node.data = data;
        }
        node.data.callback = (callback: UpdaterCallback) => {
          if (node) {
            node.registerUpdater(callback);
          }
        }
        return node;
      }
      // ...
  5. 实现recycleNode方法,回收Node组件

     // 回收Node组件,提供给下次复用
      public recycleNode(type: string, node: NodeItem) {
        let nodeArray: Array<NodeItem> = this.nodePool.get(type);
        if (!nodeArray) {
          nodeArray = new Array();
          this.nodePool.set(type, nodeArray);
        }
        nodeArray.push(node);
      }
  6. 使用NodeContainer占位轮播图组件和瀑布流子组件的位置,在最外层的Swiper切换时,会根据LazyForEach的懒加载机制回收页面,此时会触发NodeItem中的aboutToDisappear方法,将组件回收到复用池中。而新加载的页面则可以通过自定义的组件复用池获取可用的子组件,如果未获取到对应type类型的组件,则会重新创建新的组件,否则直接获取之前回收的子组件进行复用。

    // ...
    @Builder
    function FlowItemBuilder(data: ESObject) {
      FlowItemNode({
        item: data.item,
        itemHeight: data.itemHeight,
        itemColor: data.itemColor,
        updater: data.updater,
        callback: data.callback
      })
    }
    
    let flowItemWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(FlowItemBuilder);
    let swiperWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(SwiperBuilder);
    
    @Component
    export struct TabNode {
      // ...
      build() {
        Scroll(this.scroller) {
          Column({ space: 2 }) {
            NodeContainer(NodePool.getInstance().getNode('reuse_type_swiper_', {
              images: this.images
            }, swiperWrapper))
            WaterFlow() {
              LazyForEach(this.dataSource, (item: ViewItem, index: number) => {
                FlowItem() {
                  NodeContainer(NodePool.getInstance().getNode('reuse_type_', {
                    item: item,
                    itemHeight: this.itemHeightArray[index % 100],
                    itemColor: this.colors[index % 5],
                    updater: (item: ViewItem) => {
                      this.fillNewData(item);
                    },
                    callback: null
                  }, flowItemWrapper))
                }
                .width('100%')
              }, (item: string) => item)
            }
        // ...
      }
    }

编译运行后,点击Tabs切换页面,然后抓取Trace,通过图3可以看到,风景页面的首帧创建耗时和常规复用是差不多的,但是后面2个页面的耗时大幅减少,只有14ms和17ms左右。这是因为风景页面创建时自定义复用池里没有可复用的子组件,所以会和常规复用一样,直接创建新的子组件。而切换到商品页面时,首页里面的子组件被回收到了自定义复用池NodePool中,当商品页面被创建时,会先去复用池中查找可用的子组件直接使用,减少了创建子组件的时间。

图3 自定义组件复用池Trace图

img

性能数据对比

页面风景商品旅游头像
创建耗时(优化前)39.5ms35.7ms29.8ms26.5ms
创建耗时(优化后)40.3ms14.8ms17.8ms18.3ms

总结

在父组件内部进行组件复用时,使用常规复用是可以解决问题的,而且使用简单,只需要添加@Reusable装饰器并且实现aboutToReuse。但是由于复用池的局限性,不同的父组件想要复用相同子组件时就会失效。而自定义组件复用池,可以实现跨页面的组件复用,但是实现起来也比较复杂,需要开发者自己维护复用池。

FAQ

**Q:**示例代码中为什么不使用ArkUI提供的Tabs+TabContent组件,而是要用List+Swiper组件实现?

**A:**Tabs中不支持使用LazyForEach,只能使用ForEach。如果使用ForEach,那么在页面创建时会将所有的TabContent全部创建,并且切换时无法回收子组件(不会执行aboutToDisappear),这就导致自定义复用池NodePool中是空的,每次创建时都获取不到组件,只能重新创建,使组件复用失去了效果。并且因为多创建了一个NodeContainer组件,耗时会比常规复用更长。

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。 

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:

希望这一份鸿蒙学习文档能够给大家带来帮助~


 鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频教程+学习PDF文档

(鸿蒙语法ArkTS、TypeScript、ArkUI教程……)

 纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

                   

鸿蒙APP开发必备

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值