Tabs预加载的实现方式及常见问题
=================

#### 问题现象

使用Tabs组件承载内容视图时，为了优化页面加载速度、提高用户体验，通常会考虑对子页内容预加载。在实现预加载的过程中，可能会遇到以下问题场景：

*   场景一：在aboutToAppear生命周期中使用preloadItems方法预加载TabContent未生效。
    
    案例代码如下，被预加载子组件的aboutToAppear函数未执行，说明预加载未生效。MyComponent组件实现代码见文末。
    
    @Entry
    @Component
    struct PreloadOnAppear {
      @State currentIndex: number \= 1;
      private tabsController: TabsController \= new TabsController();
      contentList: string\[\] \= \['飞机', '铁路', '自驾', '地铁'\];
    
      aboutToAppear(): void {
      _  // 预加载第0、2、3个子节点，提高滑动或点击切换至这些节点时的性能_
        this.tabsController.preloadItems(\[0, 2, 3\]);
      }
    
      build() {
        Column() {
          Tabs({ index: this.currentIndex, controller: this.tabsController }) {
            ForEach(this.contentList, (item: string, index: number) \=\> {
              TabContent() {
                MyComponent({ info: item });
              }.tabBar(SubTabBarStyle.of(\`页签${index}\`));
            });
          }
          .onChange((index: number) \=\> {
            this.currentIndex \= index;
          });
        }.width('100%').height('100%');
      }
    }
    
*   场景二：当TabContent数量较多时，一次性预加载全部TabContent对性能压力大，如何分批加载子节点。

#### 背景知识

*   [Tabs](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs)：通过页签进行内容视图切换的容器组件，每个页签对应一个内容视图。
*   [TabContent](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabcontent)：仅在Tabs中使用，对应一个切换页签的内容视图。
*   [preloadItems](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs#preloaditems12)：控制Tabs预加载指定子节点。调用该接口后会一次性加载所有指定的子节点，因此为了性能考虑，建议分批加载子节点。
    *   Tabs的preloadItems需要在Tabs创建之后去调用，首次预加载推荐在Tabs的[onAppear](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-events-show-hide#onappear)生命周期中去控制。
    *   如果[TabsController](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs#tabscontroller)对象未绑定任何Tabs组件，直接调用该接口，会抛出JS异常。因此使用该接口时，建议通过try-catch捕获异常。

#### 解决方案

| 
问题场景

 | 

问题描述

 | 

解决方案

 |
| :-- | :-- | :-- |
| 

场景一

 | 

在aboutToAppear生命周期中使用preloadItems方法预加载TabContent未生效。

 | 

确保预加载在Tabs和TabsController绑定后执行，如在onAppear中执行预加载逻辑。

 |
| 

场景二

 | 

当TabContent数量较多时，一次性预加载全部TabContent对性能压力大，如何分批加载子节点。

 | 

和页签切换事件绑定，切换至某页签时，预加载当前页签的前后N项子页内容。

 |

**说明**：下述方案中使用的MyComponent代码附在文章末尾。

1.  **场景一**：在aboutToAppear生命周期中使用preloadItems方法预加载TabContent未生效。
    *   原因：预加载时机错误，[aboutToAppear](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-custom-component-lifecycle#abouttoappear)在build()函数执行前调用，此时Tabs组件未和TabsController绑定，预加载会失效。
    *   解决方案：确保预加载操作在Tabs与TabsController绑定之后执行，可参考以下生命周期函数进行实现。
        *   自定义组件的[onDidBuild](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-custom-component-lifecycle#ondidbuild12)周期函数，在自定义组件的build()函数执行后调用。
        *   Tabs的[onAppear](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-events-show-hide#onappear)生命周期，Tabs组件挂载完成后触发。
            
            以在onAppear中预加载为例：PreloadOnAppear.ets文件代码如下，@Entry页面需在resources/base/profile/main\_pages.json配置，参考[pages标签](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/module-configuration-file#pages标签)。
            
            import { BusinessError } from '@kit.BasicServicesKit';
            import { MyComponent } from './MyComponent';
            
            @Entry
            @Component
            struct PreloadOnAppear {
              @State currentIndex: number \= 1;
              private tabsController: TabsController \= new TabsController();
              contentList: string\[\] \= \['飞机', '铁路', '自驾', '地铁'\];
            
              build() {
                Column() {
                  Tabs({ index: this.currentIndex, controller: this.tabsController }) {
                    ForEach(this.contentList, (item: string, index: number) \=\> {
                      TabContent() {
                        MyComponent({ info: item });
                      }.tabBar(SubTabBarStyle.of(\`页签${index}\`));
                    });
                  }
                  .onAppear(() \=\> {
                  _  // 预加载第0、2、3个子节点，提高滑动或点击切换至这些节点时的性能_
                    this.tabsController.preloadItems(\[0, 2, 3\])
                      .then(() \=\> {
                        console.info('preloadItems success.');
                      })
                      .catch((error: BusinessError) \=\> {
                        console.error(\`preloadItems failed, error code: ${error.code}, error message: ${error.message}\`);
                      });
                  })
                  .onChange((index: number) \=\> {
                    this.currentIndex \= index;
                  });
                }.width('100%').height('100%');
              }
            }
            

2.  **场景二**：当TabContent数量较多时，一次性预加载全部TabContent对性能压力大，如何分批加载子节点。
    *   解决方案：和页签切换[onChange](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs#onchange)事件绑定，跳转指定页签后，加载当前页签前后N项子节点。
        *   以加载当前页签前后1项子节点为例：PreloadByBatches.ets文件代码如下，@Entry页面需在resources/base/profile/main\_pages.json配置，参考[pages标签](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/module-configuration-file#pages标签)。
            
            import { BusinessError } from '@kit.BasicServicesKit';
            import { MyComponent } from './MyComponent';
            
            @Entry
            @Component
            struct PreloadOnAppear {
              @State currentIndex: number \= 0;
              private tabsController: TabsController \= new TabsController();
              contentList: string\[\] \= \['飞机', '铁路', '自驾', '地铁', '公交', '骑行', '跑步', '轮船'\];
            
              build() {
                Column() {
                  Tabs({ index: this.currentIndex, controller: this.tabsController }) {
                    ForEach(this.contentList, (item: string, index: number) \=\> {
                      TabContent() {
                        MyComponent({ info: item });
                      }.tabBar(SubTabBarStyle.of(\`页签${index}\`));
                    });
                  }
                  .barMode(BarMode.Scrollable)
                  .onChange((index: number) \=\> {
                    let arr: number\[\] \= \[\];
                    if (index \> 0) {
                      arr.push(index \- 1);
                    }
                    if (index < this.contentList.length \- 1) {
                      arr.push(index \+ 1);
                    }
                  _  // 预加载当前页签的前一项和后一项，边界页签除外_
                    this.tabsController.preloadItems(arr)
                      .then(() \=\> {
                        console.info('preloadItems success.');
                      })
                      .catch((error: BusinessError) \=\> {
                        console.error(\`preloadItems failed, error code: ${error.code}, error message: ${error.message}\`);
                      });
                  })
                  .onChange((index: number) \=\> {
                    this.currentIndex \= index;
                  });
                }.width('100%').height('100%');
              }
            }
            

上述方案使用的MyComponent.ets文件代码如下：

@Component
export struct MyComponent {
  private info: string \= '';

  _// 预加载组件的aboutToAppear函数会被调用_
  aboutToAppear(): void {
    console.info(\`aboutToAppear: ${this.info}\`);
  }

  aboutToDisappear(): void {
    console.info(\`aboutToDisappear: ${this.info}\`);
  }

  build() {
    Column() {
      Text(this.info);
    }.justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%');
  }
}

#### 常见FAQ

Q：Tabs预加载的适用场景？

A：TabContent加载数据较多时，或者有网络请求等，通过预加载可以避免切换卡顿、子页加载缓慢等问题。

Q：在Tabs组件中添加多个TabContent时，节点树中TabBar的子节点数量始终比实际TabContent的数量多两个，但TabContent的数量是正确的。这种现象的原因是什么？

A：当使用ForEach动态生成TabBar时，框架会默认创建2个额外的缓存节点用于预加载优化，额外节点不会影响功能逻辑和性能。

Q：Tabs组件预加载生效之后会调用子组件的aboutToAppear方法吗？

A：被预加载的TabContent中的对应控件的aboutToAppear方法会被调用，可以添加日志来验证。

Q：为什么页面组件在TabContent加载的时候只调用aboutToAppear，不调用onPageShow？

A：onPageShow是@Entry页面的生命周期，Tabs切换触发的是组件级的生命周期aboutToAppear。