文档管理中心

自定义组件复用开发实践

概述

组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。

在应用开发时,组件复用是优化UI性能,确保应用流畅的重要手段。合理使用可复用组件,一方面,可以避免频繁创建和销毁对象的过程,减少内存回收的频率;另一方面,复用缓存中的组件可以直接绑定数据进行显示,与创建新视图相比,降低了计算开销,提升了显示效率。

常见的组件复用开发场景是长列表滑动:在应用展示大量数据的列表界面中,当用户快速地进行滑动操作,列表项反复创建销毁可能导致卡顿等性能问题。这种情况下,使用组件复用机制可以重用已经创建过的列表项视图,提高滑动的流畅度。

本文介绍以下组件复用开发场景,帮助开发者更好地理解复用机制,进而优化应用性能。

说明

本文以列表相关场景为例介绍,但实际只要发生了自定义组件的销毁和再创建,都可以考虑使用组件复用。包括以下情形:

  1. 滑动场景下对子组件进行频繁创建和销毁。例如List、Grid、WaterFlow、Swiper等布局容器中的滑动。
  2. 界面中反复切换条件渲染的控制分支,且控制分支中的子组件树结构比较复杂。

同一列表内的组件复用

场景描述

同一列表内的列表项组件复用是典型的应用开发场景。列表在滑动时,超出屏幕一定范围的列表项,被放入缓存池中,当新的列表项滑动进入屏幕范围内时,从缓存池中取出对象,绑定对应数据后呈现到列表界面中。

在实际业务中,同一列表内可能呈现一种或多种不同结构的列表项,可以进一步划分以下子场景:

实现原理

ArkUI提供了@Reusable装饰器以实现自定义组件的复用,其原理如图所示:

  1. 标记了@Reusable的自定义组件listItem列表项,在滑动出屏幕一定范围后,从组件树上被移除,组件的对象实例被放入CustomNode虚拟结点(与自定义组件一一对应的自定义结点)。
  2. 在不断滑动过程中,列表的RecycleManager将这些CustomNode虚拟结点回收,根据复用标识分组,形成CachedRecycleNodes的集合,即视图对象的复用缓存池。
  3. 继续滑动,新的listItem需要在列表上显示时,RecycleManager优先从复用缓存池(CachedRecycleNodes集合)中查找对应reuseId的视图对象,然后将新的数据绑定到该视图,重用该节点并添加到组件树上。

开发步骤

  1. 定义可复用组件:使用@Reusable装饰器修饰可复用的自定义组件。

  2. 实现复用回调:可复用组件需要实现aboutToReuse()生命周期回调。当组件从缓存中重新加入到节点树时,触发aboutToReuse()生命周期回调,组件的构造参数会传递进来,开发者根据需要在回调中处理数据刷新。

  3. 布局中使用可复用组件:设置reuseId划分组件的复用组别,以区分缓存池。未设置reuseId时,组件名会默认作为reuseId。

    收起
    自动换行
    深色代码主题
    复制
    1. @Entry
    2. @Component
    3. struct Index {
    4. // ...
    5. build() {
    6. Column() {
    7. // ...
    8. if (this.switch) {
    9. ReusableComponent({ text: this.typeStr })
    10. .reuseId(this.typeStr)
    11. }
    12. }
    13. // ...
    14. }
    15. // ...
    16. }
    17. @Reusable
    18. @Component
    19. struct ReusableComponent {
    20. @State text: string = ''
    21. aboutToReuse(params: Record<string, Object>): void {
    22. this.text = params.text as string;
    23. }
    24. build() {
    25. // ...
    26. }
    27. }
注意
  • @Reusable修饰的组件需要布局在同一个父自定义组件(后文简称“父组件”)下才能实现缓存复用。
  • 不建议在@Reusable修饰的组件中嵌套使用另一个@Reusable组件。

    更多注意事项参考指南限制条件

列表项结构类型相同

这种场景下,列表中的每一项都是由相同类型的元素和布局构成,列表项组件可以作为复用逻辑的基本单位。

实现步骤:

  1. 将列表项封装为自定义组件ItemView,添加@Reusable修饰。
  2. 在ItemView组件内的aboutToReuse()方法中进行新数据绑定逻辑。
  3. 在列表的LazyForEach()中使用ItemView组件,设置reuseId。

下面的示例中,ItemView组件添加了@Reusable和aboutToReuse()方法,然后在OneTypeItemPage页面中使用,对ItemView组件设置reuseId('item_id'),表示此处以item_id为复用分组id。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct OneTypeItemPage {
  3. // ...
  4. build() {
  5. NavDestination() {
  6. Column() {
  7. List() {
  8. LazyForEach(this.dataSource, (item: ItemData) => {
  9. ListItem() {
  10. ItemView({ title: item.title, from: item.from, tail: item.tail })
  11. .reuseId('item_id')
  12. }
  13. }, (item: ItemData) => item.id.toString())
  14. }
  15. // ...
  16. }
  17. // ...
  18. }
  19. // ...
  20. }
  21. }
  22. @Reusable
  23. @Component
  24. struct ItemView {
  25. @State title: string | Resource = '';
  26. @State from: string | Resource = '';
  27. @State tail: string | Resource = '';
  28. aboutToReuse(params: Record<string, Object>): void {
  29. this.title = params.title as string;
  30. this.from = params.from as string;
  31. this.tail = params.tail as string;
  32. }
  33. build() {
  34. // ...
  35. }
  36. }

列表项结构类型不同

这种场景下,列表中会有多种类型的列表项,如下图包含了文本、单图、多图等三种列表项,其布局、组成元素存在一定差异,可以将每种类型的列表项分别作为复用单位。

在滑动的过程中,不同类型的列表项将分别回收进入各自的缓存池,当需要复用时,根据类型找到对应视图缓存进行显示。

实现步骤:

  1. 将不同类型的列表项分别封装为自定义组件,添加@Reusable修饰。
  2. 在组件内的aboutToReuse()方法中进行新的数据绑定逻辑。
  3. 在列表的LazyForEach()中,根据业务逻辑进行if条件选择,布局相应类型的列表项组件,分别设置reuseId。
收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct MultiTypeItemPage {
  3. // ...
  4. build() {
  5. NavDestination() {
  6. Column() {
  7. List() {
  8. LazyForEach(this.dataSource, (item: ItemData) => {
  9. if (item.type === 0) {
  10. TextTypeItemView({ item: item })
  11. .reuseId('text_item_id')
  12. } else if (item.type === 1) {
  13. ImageTypeItemView({ item: item })
  14. .reuseId('image_item_id')
  15. } else if (item.type === 2) {
  16. ThreeImageTypeItemView({ item: item })
  17. .reuseId('three_image_item_id')
  18. }
  19. }, (item: ItemData) => item.id.toString())
  20. }
  21. // ...
  22. }
  23. // ...
  24. }
  25. // ...
  26. }
  27. }
  28. @Reusable
  29. @Component
  30. struct TextTypeItemView {
  31. // ...
  32. }
  33. @Reusable
  34. @Component
  35. struct ImageTypeItemView {
  36. // ...
  37. }
  38. @Reusable
  39. @Component
  40. struct ThreeImageTypeItemView {
  41. // ...
  42. }

列表项内子组件可拆分组合

这种情况下,列表项也具有多种结构类型。通过观察可知,列表项内部子组件都是纵向分布排列,相同之处是顶部的文本标题、底部的发布时间,而不同之处是中间的区域部分:有单图、多图、视频三种情况。

因此,可以创建五种复用子组件,通过子组件的选择组合,实现不同类型的列表项。

实现步骤:

  1. 将单图、多图、视频、顶部标题、底部时间等分别封装为子组件,添加@Reusable修饰。
  2. 在组件内的aboutToReuse()方法中进行新的数据绑定逻辑。
  3. 通过组合子组件,实现三个不同的@Builder函数,与三种列表项一一对应。
  4. 在列表的LazyForEach()中,根据业务逻辑进行条件选择,分别调用相应类型的@Builder函数。
说明

使用 @Builder 而非直接嵌套子组件的原因如下:

由于缓存池位于自定义组件上,嵌套子组件后会将缓存池分割,导致复用不生效。而使用@Builder可以使内部的自定义组件依然汇聚在同一个缓存池里,从而实现相互复用。

正例:

itemBuilderSingleImage()函数使用@Builder装饰器实现,ComposableItemPage中调用this.itemBuilderSingleImage(item)布局,itemBuilderSingleImage()内部的自定义组件就会汇聚在ComposableItemPage对应的缓存池里,即TopView、MiddleSingleImageView、BottomView等Reusable组件汇聚在同一缓存池里,当调用ComposableItemPage中其他@Builder函数时,也可复用内部@Reusable装饰的组件。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct ComposableItemPage {
  3. // ...
  4. @Builder
  5. itemBuilderSingleImage(item: ItemData) {
  6. TopView({ item: item }).reuseId('top_id')
  7. MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
  8. BottomView({ item: item }).reuseId('bottom_id')
  9. }
  10. @Builder
  11. itemBuilderThreeImage(item: ItemData) {
  12. TopView({ item: item }).reuseId('top_id')
  13. MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
  14. BottomView({ item: item }).reuseId('bottom_id')
  15. }
  16. @Builder
  17. itemBuilderVideoImage(item: ItemData) {
  18. TopView({ item: item }).reuseId('top_id')
  19. MiddleVideoView({ item: item }).reuseId('middle_video_id')
  20. BottomView({ item: item }).reuseId('bottom_id')
  21. }
  22. build() {
  23. NavDestination() {
  24. Column() {
  25. List() {
  26. LazyForEach(this.dataSource, (item: ItemData) => {
  27. ListItem() {
  28. Column() {
  29. if (item.type === 0) {
  30. this.itemBuilderSingleImage(item)
  31. } else if (item.type === 1) {
  32. this.itemBuilderThreeImage(item)
  33. } else if (item.type === 2) {
  34. this.itemBuilderVideoImage(item)
  35. }
  36. }
  37. // ...
  38. }
  39. }, (item: ItemData) => item.id.toString())
  40. }
  41. // ...
  42. }
  43. // ...
  44. }
  45. // ...
  46. }
  47. }
  48. @Reusable
  49. @Component
  50. struct TopView {
  51. // ...
  52. }
  53. @Reusable
  54. @Component
  55. struct BottomView {
  56. // ...
  57. }
  58. @Reusable
  59. @Component
  60. struct MiddleSingleImageView {
  61. // ...
  62. }
  63. @Reusable
  64. @Component
  65. struct MiddleThreeImageView {
  66. // ...
  67. }
  68. @Reusable
  69. @Component
  70. struct MiddleVideoView {
  71. // ...
  72. }

反例:

如果将itemBuilderSingleImage()、itemBuilderThreeImage()等@Builder函数改为@Component装饰器实现,比如分别命名为SingleImageComponent、ThreeImageComponent等,这会导致SingleImageComponent内部的TopView、MiddleSingleImageView、BottomView等组件仅汇聚在SingleImageComponent组件对应的缓存池中,ThreeImageComponent内部同理。这些@Reusable组件因处在不同@Component的缓存池中,ComposableItemPage在最后布局绘制时,复用将不生效。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct ComposableItemPage {
  3. // ...
  4. build() {
  5. NavDestination() {
  6. Column() {
  7. List() {
  8. LazyForEach(this.dataSource, (item: ItemData) => {
  9. ListItem() {
  10. Column() {
  11. if (item.type === 0) {
  12. SingleImageComponent({ item: item })
  13. } else if (item.type === 1) {
  14. ThreeImageComponent({ item: item })
  15. } else if (item.type === 2) {
  16. VideoComponent({ item: item })
  17. }
  18. }
  19. // ...
  20. }
  21. }, (item: ItemData) => item.id.toString())
  22. }
  23. // ...
  24. }
  25. // ...
  26. }
  27. // ...
  28. }
  29. }
  30. @Component
  31. struct SingleImageComponent{
  32. // ...
  33. build() {
  34. Column() {
  35. TopView({ item: item }).reuseId('top_id')
  36. MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
  37. BottomView({ item: item }).reuseId('bottom_id')
  38. }
  39. }
  40. }
  41. @Component
  42. struct ThreeImageComponent{
  43. // ...
  44. build() {
  45. Column() {
  46. TopView({ item: item }).reuseId('top_id')
  47. MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
  48. BottomView({ item: item }).reuseId('bottom_id')
  49. }
  50. }
  51. }
  52. @Component
  53. struct VideoComponent{
  54. // ...
  55. build() {
  56. Column() {
  57. TopView({ item: item }).reuseId('top_id')
  58. MiddleVideoView({ item: item }).reuseId('middle_video_id')
  59. BottomView({ item: item }).reuseId('bottom_id')
  60. }
  61. }
  62. }
  63. @Reusable
  64. @Component
  65. struct TopView {
  66. // ...
  67. }
  68. @Reusable
  69. @Component
  70. struct BottomView {
  71. // ...
  72. }
  73. @Reusable
  74. @Component
  75. struct MiddleSingleImageView {
  76. // ...
  77. }
  78. @Reusable
  79. @Component
  80. struct MiddleThreeImageView {
  81. // ...
  82. }
  83. @Reusable
  84. @Component
  85. struct MiddleVideoView {
  86. // ...
  87. }

多个列表间的组件复用

场景描述

应用开发有这种场景:在不同的标题页面中展示数据,每一页面下实现了一个列表,这样在页面切换时,列表与列表之间如果存在结构相同的列表项,就有组件复用的优化可能。例如下图,News、Hot等页签下,绘制了类型相同的列表项。

实现原理

在ArkUI中,可以采用Swiper+List实现这种功能场景,其中Swiper中的每个页面都使用一个List列表呈现内容。从@Reusable的复用机制可知,复用缓存池需要在同一父组件中,而列表项Item的父组件是当前页面的列表List,当Swiper内的页面切换时,无法直接复用上一个页面的列表项。

此时可以自定义一个全局的复用缓存池NodePool,利用BuilderNode的节点复用能力,根据页面状态创建、回收、复用子组件,实现这种跨页面多个列表间的组件复用。

说明

为什么不使用Tabs+List,而是用Swiper+List组件实现?

当前Tabs内容页不支持使用LazyForEach(),只能使用ForEach+TabContent。如果使用ForEach(),Tabs页面显示时会一次性将所有的TabContent创建,TabContent子页面切换时也不会执行aboutToDisappear(),无法回收组件,进而不存在复用优化的可能。

在需要布局自定义组件的位置,使用NodeContainer占位,然后继承NodeController实现NodeItem结点类,其内部需要持有BuilderNode实例以实现结点的创建和复用,同时需要持有视图相应的数据对象以更新界面显示。

  1. 当NodeItem随着视图组件即将销毁时,在aboutToDisappear()中回收NodeItem到NodePool缓存池,存入type类型对应的集合中。
  2. 每次需要创建自定义组件时,优先根据type类型查找对应的NodeItem对象,若未找到则新建一个NodeItem。
  3. 视图组件随着NodeContainer的生命周期显示时,执行数据更新,完成组件的复用过程。

这种方式需要自行维护复用池,开发者也可以考虑使用同一原理实现的全局组件复用池三方库:nodepool

开发步骤

  1. 实现列表项占位结点类NodeItem,继承NodeController实现makeNode()方法,根据node是否存在,执行创建或刷新数据的逻辑,并在aboutToDisappear()时回收组件结点。

    收起
    自动换行
    深色代码主题
    复制
    1. export class NodeItem extends NodeController {
    2. public builder: WrappedBuilder<ESObject> | null = null;
    3. public node: BuilderNode<ESObject> | null = null;
    4. public data: ESObject = {};
    5. public type: string = '';
    6. public id: number = 0;
    7. aboutToRecycle(): void {
    8. NodePool.getInstance().recycleNode(this.type, this);
    9. }
    10. update(data: ESObject) {
    11. this.data = data;
    12. this.node?.reuse(data);
    13. }
    14. makeNode(uiContext: UIContext): FrameNode | null {
    15. if (!this.node) {
    16. this.node = new BuilderNode(uiContext);
    17. this.node.build(this.builder, this.data);
    18. } else {
    19. this.update(this.data);
    20. }
    21. return this.node.getFrameNode();
    22. }
    23. // ...
    24. }
  2. 使用单例模式实现复用缓存池NodePool工具类,在应用内统一管理组件的复用逻辑:实现取缓存getNode()方法,根据传入的type类型,获取对应的NodeItem,如果未找到,则新创建后绑定数据;实现缓存回收recycleNode()方法,根据type类型存入相应的集合中。

    收起
    自动换行
    深色代码主题
    复制
    1. export class NodePool {
    2. private static instance: NodePool;
    3. private idGen: number;
    4. private nodePool: HashMap<string, LinkedList<NodeItem>>;
    5. private constructor() {
    6. this.nodePool = new HashMap();
    7. this.idGen = 0;
    8. }
    9. public static getInstance() {
    10. if (!NodePool.instance) {
    11. NodePool.instance = new NodePool();
    12. }
    13. return NodePool.instance;
    14. }
    15. public getNextId(): number {
    16. this.idGen += 1;
    17. return this.idGen;
    18. }
    19. public getNode(type: string, item: ESObject,
    20. builder: WrappedBuilder<ESObject>): NodeItem | undefined {
    21. let nodeItem: NodeItem | undefined = undefined;
    22. try {
    23. if (this.nodePool.get(type)) {
    24. for (let i = 0; i < this.nodePool.get(type)?.length; i++) {
    25. let tmpItem: NodeItem | undefined = this.nodePool.get(type)?.get(i);
    26. if (!tmpItem.node?.getFrameNode()?.getParent()) {
    27. nodeItem = tmpItem;
    28. this.nodePool.get(type)?.removeByIndex(i);
    29. break;
    30. }
    31. }
    32. }
    33. } catch (e) {
    34. let err = e as BusinessError;
    35. hilog.error(DOMAIN, 'testTag', `failed code=${err.code}, message=${err.message}`);
    36. }
    37. if (!nodeItem) {
    38. nodeItem = new NodeItem();
    39. nodeItem.builder = builder;
    40. nodeItem.type = type;
    41. nodeItem.data.item = item;
    42. } else {
    43. nodeItem.data.item = item;
    44. }
    45. return nodeItem;
    46. }
    47. public recycleNode(type: string, node: NodeItem) {
    48. try {
    49. let nodeArray: LinkedList<NodeItem> = this.nodePool.get(type);
    50. if (!nodeArray) {
    51. nodeArray = new LinkedList();
    52. this.nodePool.set(type, nodeArray);
    53. }
    54. node.data.item = {};
    55. nodeArray.add(node);
    56. } catch (e) {
    57. let err = e as BusinessError;
    58. hilog.error(DOMAIN, 'testTag', `failed code=${err.code}, message=${err.message}`);
    59. }
    60. }
    61. // ...
    62. }
    注意
    • getNode()方法中,如果找到的NodeItem父结点不为空(说明未完全下树),需要继续遍历查找下一个有效的NodeItem对象。
    • recycleNode()方法中,需要对NodeItem对象属性重置,使节点内容还原,避免复用显示异常情况。
  3. 将步骤1中的列表项占位结点包装成组件,在对应的生命周期中分别取缓存、回收、复用。

    收起
    自动换行
    深色代码主题
    复制
    1. @Component
    2. export struct DiffListItemNode {
    3. @State type: string = '';
    4. @State item: ItemData = new ItemData('', 0);
    5. @State itemHeight: number = 0;
    6. @State builder: WrappedBuilder<ESObject> | null = null;
    7. private nodeItem: NodeItem = new NodeItem();
    8. aboutToAppear(): void {
    9. this.nodeItem = NodePool.getInstance().getNode(this.type, this.item, this.builder!)!;
    10. }
    11. aboutToDisappear(): void {
    12. if (this.nodeItem === undefined) {
    13. return;
    14. }
    15. this.nodeItem?.aboutToRecycle();
    16. }
    17. aboutToRecycle(): void {
    18. this.nodeItem?.node?.recycle();
    19. }
    20. aboutToReuse(params: ESObject): void {
    21. this.nodeItem?.node?.reuse(params);
    22. }
    23. build() {
    24. NodeContainer(this.nodeItem)
    25. }
    26. }
  4. 封装列表项的界面视图组件,使用listItemBuilder函数对外export该组件。

    收起
    自动换行
    深色代码主题
    复制
    1. @Builder
    2. export function listItemBuilder(data: ESObject) {
    3. DiffListItemView({
    4. item: data.item
    5. })
    6. }
    7. @Component
    8. export struct DiffListItemView {
    9. // ...
    10. aboutToReuse(params: ESObject): void {
    11. this.item = params.item;
    12. }
    13. build() {
    14. Row() {
    15. // ...
    16. }
    17. // ...
    18. }
    19. }
  5. 在列表的LazyForEach()中,将步骤4的实际列表项视图wrapBuilder后作为参数传递给步骤3封装的占位组件,实现复用组件的布局。

    收起
    自动换行
    深色代码主题
    复制
    1. export const listItemWrapper: WrappedBuilder<ESObject> = wrapBuilder<ESObject>(listItemBuilder);
    2. @Component
    3. export struct TabContentView {
    4. // ...
    5. build() {
    6. List() {
    7. LazyForEach(this.dataSource, (item: ItemData) => {
    8. DiffListItemNode({
    9. type: REUSE_VIEW_TYPE_ITEM,
    10. item: item,
    11. builder: listItemWrapper
    12. })
    13. }, (item: ItemData) => item.id.toString())
    14. }
    15. // ...
    16. }
    17. }

使用onIdle()预创建组件

在当前场景下,首次进入页面可能耗时较高,因为在第一次进入时,自定义组件复用池中没有缓存可以复用,列表项都需要新创建。优化这个问题,可以考虑预创建组件,将组件对象提前放入复用缓存池中。

当组件数量较多,集中预创建本身也耗时较长,容易导致主线程阻塞。ArkUI中提供了onIdle()接口,会返回每一帧帧尾的空闲时间,可以将组件预创建分布到每一帧帧尾的空闲时间中执行,这样预创建过程就被平摊在多个周期里执行,避免集中运行的耗时影响,进而优化应用体验。

注意
  1. 需要根据业务准确预估组件预创建耗时,同时将业务逻辑颗粒度拆小,以便能够分到多个onIdle()时机中完成。例如,单个组件预创建耗时在2ms左右,帧尾空闲时间只有1ms,那么就不能在当前帧进行预创建,而是延迟到下一帧中执行。
  2. 需要合理控制自定义组件复用池中预创建的数量,否则内存占用较多,可能会影响性能。
  1. 在NodePool工具类中实现预创建preBuild()方法:新建NodeItem实例,设置builder等属性,执行recycleNode()提前放入缓存池中。

    收起
    自动换行
    深色代码主题
    复制
    1. export class NodeItem extends NodeController {
    2. // ...
    3. prebuild(uiContext: UIContext) {
    4. this.node = new BuilderNode(uiContext);
    5. this.node.build(this.builder, this.data);
    6. }
    7. }
    8. export class NodePool {
    9. // ...
    10. public preBuild(type: string, item: ESObject, builder: WrappedBuilder<ESObject>, uiContext: UIContext) {
    11. if (type) {
    12. let nodeItem: NodeItem | undefined = new NodeItem();
    13. nodeItem.builder = builder;
    14. nodeItem.data.item = item;
    15. nodeItem.type = type;
    16. nodeItem.prebuild(uiContext);
    17. this.recycleNode(type, nodeItem);
    18. }
    19. }
    20. }
  2. 继承FrameCallback实现帧回调类,在构造器中传入预创建的数据,并实现onIdle()接口。

    1. 系统会通过onIdle()回调,将帧尾空闲时间通过参数idleTimeInNano传递出来,可根据单个组件的预创建耗时,设置预创建的剩余空闲时间上限(示例代码假设单个组件预创建耗时最长1ms=1000000ns)。
    2. 当剩余空闲时间足够创建组件时,在这一帧中进行组件预创建,并不断更新当前帧的剩余空闲时间。
    3. 若当前帧剩余空闲时间不足以创建组件,通过postFrameCallback()方法,将回调传递到下一帧,继续进行剩余组件的预创建。
      收起
      自动换行
      深色代码主题
      复制
      1. export class IdleCallback extends FrameCallback {
      2. private uiContext: UIContext;
      3. private todoCount: number = 0;
      4. private dataArray: ItemData[] = [];
      5. constructor(context: UIContext, preBuildData: ItemData[]) {
      6. super();
      7. this.uiContext = context;
      8. this.dataArray = preBuildData;
      9. }
      10. onIdle(idleTimeInNano: number): void {
      11. if (this.todoCount >= this.dataArray.length) {
      12. return;
      13. }
      14. let cur: number = systemDateTime.getTime(true);
      15. let timeLeft = idleTimeInNano;
      16. while (timeLeft >= 1000000) {
      17. hiTraceMeter.startTrace('onIdle_prebuild', 1);
      18. NodePool.getInstance().preBuild('reuse_type_', this.dataArray[this.todoCount], listItemWrapper, this.uiContext);
      19. hiTraceMeter.finishTrace('onIdle_prebuild', 1);
      20. let now = systemDateTime.getTime(true);
      21. timeLeft = timeLeft - (now - cur);
      22. cur = now;
      23. this.todoCount++;
      24. if (this.todoCount >= this.dataArray.length) {
      25. return;
      26. }
      27. if (this.todoCount < this.dataArray.length) {
      28. this.uiContext.postFrameCallback(this);
      29. }
      30. }
      31. }
      32. }
  3. 在进入Swiper+List的页面之前,选择合适的时机执行context.postFrameCallback(),开启IdleCallback帧回调逻辑。

    收起
    自动换行
    深色代码主题
    复制
    1. @Entry
    2. @Component
    3. struct Index {
    4. // ...
    5. aboutToAppear(): void {
    6. let dataArray: ItemData[] = [];
    7. dataArray.push(...genMockItemData(100))
    8. let context = this.getUIContext();
    9. context.postFrameCallback(new IdleCallback(context, dataArray));
    10. }
    11. // ...
    12. }

更多优化方法

使用attributeUpdater实现组件属性的部分刷新

在可复用组件中使用attributeUpdater可以控制指定属性的刷新,避免不必要的重绘、减少渲染负载,从而提升应用性能。

默认情况下,直接使用状态变量在aboutToReuse()中进行数据赋值,会导致组件全部属性刷新。实际需求中,可能只需要更新组件的部分属性。例如,当组件复用显示时,只希望设置文本的字体颜色,那么可以使用attributeUpdater在aboutToReuse()修改fontColor属性。

反例:

这里通过aboutToReuse()对fontColor状态变量更新,导致组件全部属性进行刷新,造成不必要的耗时。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct LessEmbeddedComponent {
  3. aboutToAppear(): void {
  4. momentData.getFriendMomentFromRawfile();
  5. }
  6. build() {
  7. Column() {
  8. Text('use nothing')
  9. List({ space: ListConstants.LIST_SPACE }) {
  10. LazyForEach(momentData, (moment: FriendMoment) => {
  11. ListItem() {
  12. OneMomentNoModifier({ color: moment.color })
  13. .onClick(() => {
  14. })
  15. }
  16. }, (moment: FriendMoment) => moment.id)
  17. }
  18. .width("100%")
  19. .height("100%")
  20. .cachedCount(5)
  21. }
  22. }
  23. }
  24. @Reusable
  25. @Component
  26. export struct OneMomentNoModifier {
  27. @State color: string | number | Resource = "";
  28. aboutToReuse(params: Record<string, Object>): void {
  29. this.color = params.color as number;
  30. }
  31. build() {
  32. Column() {
  33. Text('This is title')
  34. Text('This is desc text')
  35. .fontColor(this.color)
  36. .textAlign(TextAlign.Center)
  37. .fontStyle(FontStyle.Normal)
  38. .fontSize(13)
  39. .lineHeight(30)
  40. .opacity(0.6)
  41. .margin({ top: 10 })
  42. .fontWeight(30)
  43. .clip(false)
  44. .backgroundBlurStyle(BlurStyle.NONE)
  45. .foregroundBlurStyle(BlurStyle.NONE)
  46. .borderWidth(1)
  47. .borderColor(Color.Pink)
  48. .borderStyle(BorderStyle.Solid)
  49. .alignRules({
  50. 'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
  51. 'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
  52. })
  53. }
  54. }
  55. }

正例:

通过attributeUpdater对Text组件中的fontColor属性进行精准刷新,避免重绘Text中不需要更改的属性。

收起
自动换行
深色代码主题
复制
  1. export class MyTextUpdater extends AttributeUpdater<TextAttribute> {
  2. private color: string | number | Resource | Color = '';
  3. constructor(color: string | number | Resource | Color) {
  4. super();
  5. this.color = color;
  6. }
  7. initializeModifier(instance: TextAttribute): void {
  8. instance.fontColor(this.color)
  9. }
  10. }
  11. // ...
  12. @Reusable
  13. @Component
  14. export struct OneMomentNoModifier {
  15. @State text: string = '';
  16. color: string | number | Resource | Color = '';
  17. textUpdater: MyTextUpdater | null = null;
  18. aboutToAppear(): void {
  19. this.textUpdater = new MyTextUpdater(this.color);
  20. }
  21. aboutToReuse(params: Record<string, Object>): void {
  22. this.color = params.color as string;
  23. this.text = params.text as string;
  24. this.textUpdater?.attribute?.fontColor(this.color);
  25. }
  26. build() {
  27. Column() {
  28. Text(this.text)
  29. .fontSize(18)
  30. .textAlign(TextAlign.Center)
  31. .fontWeight(500)
  32. .lineHeight(24)
  33. .fontColor(Color.Black)
  34. .opacity(0.6)
  35. Column() {
  36. Text('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
  37. .attributeModifier(this.textUpdater)
  38. .textAlign(TextAlign.Start)
  39. .fontSize(16)
  40. .fontWeight(400)
  41. .lineHeight(21)
  42. }
  43. // ...
  44. }
  45. // ...
  46. }
  47. }

使用@Link/@ObjectLink替代@Prop以减少深拷贝

在可复用组件中,建议使用@Link/@ObjectLink替代@Prop。因为@Prop装饰变量时会进行深拷贝,增加了创建时间及内存消耗,而改用@Link/@ObjectLink,变量会共享同一地址。

反例:

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct lessEmbeddedComponent {
  4. aboutToAppear(): void {
  5. getFriendMomentFromRawfile();
  6. }
  7. build() {
  8. Column() {
  9. TopBar()
  10. List({ space: ListConstants.LIST_SPACE }) {
  11. LazyForEach(momentData, (moment: FriendMoment) => {
  12. ListItem() {
  13. OneMoment({moment: moment})
  14. }
  15. }, (moment: FriendMoment) => moment.id)
  16. }
  17. .cachedCount(Constants.CACHED_COUNT)
  18. }
  19. }
  20. }
  21. @Reusable
  22. @Component
  23. export struct OneMoment {
  24. @Prop moment: FriendMoment;
  25. build() {
  26. Column() {
  27. ...
  28. Text(`${this.moment.userName}`)
  29. ...
  30. }
  31. }
  32. }
  33. export class FriendMoment {
  34. id: string;
  35. userName: string;
  36. avatar: string;
  37. text: string;
  38. size: number;
  39. image?: string;
  40. }

正例:

将子组件moment变量@Prop改为@ObjectLink即可。

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

避免对@Link/@ObjectLink/@Prop等自动更新的状态变量,在aboutToReuse()中重复赋值

如果可复用组件中使用了@Link/@ObjectLink/@Prop等自动同步父子组件数据的状态变量,则不需要在aboutToReuse()中对这些数据重复赋值。如果重新赋值这些变量,会导致组件的内容重新触发状态刷新,造成额外的计算更新耗时。

反例:

收起
自动换行
深色代码主题
复制
  1. @Reusable
  2. @Component
  3. export struct OneMoment {
  4. @ObjectLink moment: FriendMoment;
  5. aboutToReuse(params: Record<string, Object>): void {
  6. this.moment.id = (params.moment as FriendMoment).id
  7. this.moment.userName = (params.moment as FriendMoment).userName
  8. this.moment.avatar = (params.moment as FriendMoment).avatar
  9. this.moment.text = (params.moment as FriendMoment).text
  10. this.moment.image = (params.moment as FriendMoment).image
  11. }
  12. build() {
  13. Column() {
  14. ...
  15. Text(`${this.moment.userName}`)
  16. ...
  17. }
  18. }
  19. }

正例:

将aboutToReuse()中的赋值语句删除。

@ObjectLink修饰的moment状态变量已包含自动刷新功能,不需要再重复赋值刷新。

使用reuseId标记布局发生变化的组件

在同一段自定义组件代码中,如果使用if/else条件语句控制布局结构,会导致在不同逻辑分支中创建不同布局的组件,从而造成组件树结构的差异。此时可以使用reuseId来区分发生变化的分支逻辑,确保系统能够根据reuseId缓存各种结构的组件,提升复用性能。

反例:

组件通过if条件创建包含Image的Flex组件。不使用reuseId时,复用后根据if条件,可能会删除Flex或重新创建Flex,存在性能消耗。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct AboutReuseId {
  4. build() {
  5. Column() {
  6. TopBar()
  7. List({ space: ListConstants.LIST_SPACE }) {
  8. LazyForEach(momentData, (moment: FriendMoment) => {
  9. ListItem() {
  10. OneMoment({
  11. moment: moment,
  12. fontSize: moment.size
  13. })
  14. }
  15. }, (moment: FriendMoment) => moment.id)
  16. }
  17. .cachedCount(Constants.CACHED_COUNT)
  18. }
  19. }
  20. }
  21. @Reusable
  22. @Component
  23. export struct OneMoment {
  24. @Prop moment: FriendMoment;
  25. build() {
  26. Column() {
  27. ...
  28. Text(this.moment.text)
  29. if (this.moment.image !== '') {
  30. Flex({ wrap: FlexWrap.Wrap }) {
  31. Image($r(this.moment.image))
  32. Image($r(this.moment.image))
  33. Image($r(this.moment.image))
  34. }
  35. }
  36. ...
  37. }
  38. }
  39. }

正例:

根据分支逻辑设置不同的reuseId,缓存不同布局结构下的组件,省去重复执行if的删除或创建逻辑。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct WithReuseId {
  4. // ...
  5. build() {
  6. Column() {
  7. List({ space: this.LIST_SPACE }) {
  8. LazyForEach(this.momentData, (moment: FriendMoment) => {
  9. ListItem() {
  10. OneMoment({ moment: moment })
  11. .reuseId((moment.image !== '') ? 'withImage_id' : 'noImage_id')
  12. }
  13. }, (moment: FriendMoment) => moment.id)
  14. }
  15. // ...
  16. }
  17. }
  18. }
  19. @Reusable
  20. @Component
  21. export struct OneMoment {
  22. @ObjectLink moment: FriendMoment;
  23. build() {
  24. Column() {
  25. // ...
  26. if (this.moment.image !== '') {
  27. Flex({ wrap: FlexWrap.Wrap }) {
  28. Image($r(this.moment.image))
  29. .width(Constants.LAYOUT_MAX)
  30. .height('27.5%')
  31. .borderRadius(16)
  32. Image($r(this.moment.image))
  33. .width(Constants.LAYOUT_MAX)
  34. .height('27.5%')
  35. .borderRadius(16)
  36. .margin({ top: 10 })
  37. }
  38. .width(Constants.LAYOUT_MAX)
  39. .margin({ top: 14 })
  40. }
  41. }
  42. // ...
  43. }
  44. }

避免使用函数方法作为复用组件的入参

如果可复用组件的入参使用了函数方法,因每次复用都需要重新创建组件关联的数据对象,该函数会在每次复用时执行,造成性能问题。建议改为通过状态变量传递参数,从而减少重复执行入参中的函数所带来的性能消耗。

反例:

复用的子组件参数sum是通过模拟耗时函数countAndReturn()生成。该函数在每次组件复用时都执行,会造成性能问题,甚至导致列表滑动过程中的卡顿丢帧。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct withFuncParam {
  4. aboutToAppear(): void {
  5. getFriendMomentFromRawfile();
  6. }
  7. countAndReturn(): number {
  8. let temp: number = 0;
  9. for (let index = 0; index < 100000; index++) {
  10. temp += index;
  11. }
  12. return temp;
  13. }
  14. build() {
  15. Column() {
  16. TopBar()
  17. List({ space: ListConstants.LIST_SPACE }) {
  18. LazyForEach(momentData, (moment: FriendMoment) => {
  19. ListItem() {
  20. OneMoment({
  21. moment: moment,
  22. sum: this.countAndReturn()
  23. })
  24. }
  25. }, (moment: FriendMoment) => moment.id)
  26. }
  27. .cachedCount(Constants.CACHED_COUNT)
  28. }
  29. }
  30. }
  31. @Reusable
  32. @Component
  33. export struct OneMoment {
  34. @Prop moment: FriendMoment;
  35. @State sum: number = 0;
  36. aboutToReuse(params: Record<string, Object>): void {
  37. this.sum = params.sum as number;
  38. }
  39. build() {
  40. Column() {
  41. ...
  42. Text(`${this.moment.userName}${this.moment.id} / ${this.sum})`)
  43. ...
  44. }
  45. }
  46. }

正例:

可以先将countAndReturn()计算放到页面初始时执行,将结果赋值给this.sum变量。在复用组件的参数传递时,通过this.sum来进行。

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct WithFuncParam {
  4. @State sum: number = 0;
  5. // ...
  6. private readonly MOCK_ASYNC_DEFAULT_NUM: number = 1000000;
  7. private readonly MOCK_ASYNC_TIME_OUT_NUM: number = 2000;
  8. aboutToAppear(): void {
  9. this.momentData.getFriendMomentFromRawFile();
  10. this.countAndReturn();
  11. }
  12. async countAndReturn(): Promise<void> {
  13. await this.sleep();
  14. this.sum = this.MOCK_ASYNC_DEFAULT_NUM;
  15. }
  16. sleep(): Promise<string> {
  17. return new Promise<string>((resolve) => {
  18. setTimeout(() => {
  19. resolve('ok');
  20. }, this.MOCK_ASYNC_TIME_OUT_NUM)
  21. });
  22. }
  23. build() {
  24. Column() {
  25. List({ space: this.LIST_SPACE }) {
  26. LazyForEach(this.momentData, (moment: FriendMoment) => {
  27. ListItem() {
  28. OneMoment({
  29. moment: moment,
  30. sum: this.sum
  31. })
  32. }
  33. }, (moment: FriendMoment) => moment.id)
  34. }
  35. // ...
  36. }
  37. }
  38. }
  39. @Reusable
  40. @Component
  41. export struct OneMoment {
  42. @ObjectLink moment: FriendMoment;
  43. @State sum: number = 0;
  44. // ...
  45. aboutToReuse(params: Record<string, Object>): void {
  46. this.sum = params.sum as number;
  47. }
  48. // ...
  49. }

常见问题

如何检查组件复用是否生效

示例代码

在 指南 中进行搜索
请输入您想要搜索的关键词