在 ArkTS 的開發中,如果你要渲染一個很長的列表,比如商品列表、評論列表或者朋友圈動態,用傳統的循環結構(比如 ForEach
)很容易導致性能問題,尤其是加載慢、卡頓甚至內存暴漲。
這時候就要用到 懶加載渲染組件——LazyForEach
。
LazyForEach
是 ArkTS 提供的一種 延遲渲染列表項的方式。只有當列表項真正要被顯示在屏幕上時,相關組件才會被創建和渲染,從而節省內存和提升性能。
可以把它理解成 ArkTS 中的“虛擬滾動列表”。
LazyForEach的使用限制
- LazyForEach必須在容器組件內使用,僅有List、Grid、Swiper以及WaterFlow組件支持數據懶加載(可配置cachedCount屬性,即只加載可視部分以及其前后少量數據用于緩沖),其他組件仍然是一次性加載所有的數據。支持數據懶加載的父組件根據自身及子組件的高度或寬度計算可視區域內需布局的子節點數量,高度或寬度的缺失會導致部分場景懶加載失效。
catchdCount
List設置cachedCount后,顯示區域外上下各會預加載并布局cachedCount行ListItem。
- LazyForEach依賴生成的鍵值判斷是否刷新子組件,鍵值不變則不觸發刷新。
- 容器組件內只能包含一個LazyForEach。以List為例,不推薦同時包含ListItem、ForEach、LazyForEach。也不推薦同時包含多個LazyForEach。
- LazyForEach在每次迭代中,必須創建且只允許創建一個子組件;即LazyForEach的子組件生成函數有且只有一個根組件。
- 生成的子組件必須是允許包含在LazyForEach父容器組件中的子組件。
- 允許LazyForEach包含在if/else條件渲染語句中,也允許LazyForEach中出現if/else條件渲染語句。
- 鍵值生成器必須針對每個數據生成唯一的值,如果鍵值相同,將導致鍵值相同的UI組件渲染出現問題。
- LazyForEach必須使用DataChangeListener對象進行更新。重新賦值第一個參數dataSource會導致異常;dataSource使用狀態變量時,狀態變量改變不會觸發LazyForEach的UI刷新。
- 為了高性能渲染,使用DataChangeListener對象的onDataChange方法更新UI時,需要生成不同于原來的鍵值來觸發組件刷新。
LazyForEach鍵值生成規則
LazyForEach提供了參數keyGenerator,開發者可以使用該函數生成自定義鍵值。如果未定義keyGenerator函數,ArkUI框架將使用默認的鍵值生成函數:(item: Object, index: number) => { return viewId + ‘-’ + index.toString(); }。viewId在編譯器轉換過程中生成,同一個LazyForEach組件內的viewId一致。
基本語法
LazyForEach(dataSource: IDataSource, // 需要進行數據迭代的數據源itemGenerator: (item: any, index?: number) => void, // 子組件生成函數keyGenerator?: (item: any, index?: number) => string // 鍵值生成函數
): void
-
dataSource:IDataSource
LazyForEach數據源,需要開發者實現相關接口。
-
itemGenerator:(item: any, index: number) => void
子組件生成函數,為數組中的每一個數據項創建一個子組件。
-
keyGenerator: (item: any, index: number) => string
鍵值生成函數,用于給數據源中的每一個數據項生成唯一且固定的鍵值。修改數據源中的一個數據項若不影響其生成的鍵值,則對應組件不會被更新,否則此處組件就會被重建更新。keyGenerator參數是可選的,但是,為了使開發框架能夠更好地識別數組更改并正確更新組件,建議提供。
實現IDataSource接口
interface IDataSource {totalCount(): number; // 獲得數據總數getData(index: number): Object; // 獲取索引值對應的數據registerDataChangeListener(listener: DataChangeListener): void; // 注冊數據改變的監聽器unregisterDataChangeListener(listener: DataChangeListener): void; // 注銷數據改變的監聽器
}
DataChangeListener接口
interface DataChangeListener {onDataReloaded(): void; // 重新加載數據完成后調用onDataAdded(index: number): void; // 添加數據完成后調用onDataMoved(from: number, to: number): void; // 數據移動起始位置與數據移動目標位置交換完成后調用onDataDeleted(index: number): void; // 刪除數據完成后調用onDataChanged(index: number): void; // 改變數據完成后調用onDataAdd(index: number): void; // 添加數據完成后調用onDataMove(from: number, to: number): void; // 數據移動起始位置與數據移動目標位置交換完成后調用onDataDelete(index: number): void; // 刪除數據完成后調用onDataChange(index: number): void; // 改變數據完成后調用 }
創建LazyForEach
首次渲染
在LazyForEach首次渲染時,會根據上述鍵值生成規則為數據源的每個數組項生成唯一鍵值并創建相應的組件。
局部代碼如下:
class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {return this.dataArray[index];}public pushData(data: string): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`);}}build() {List({ space: 3 }) { // 必須在容器組件內LazyForEach(this.data, (item: string) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info(`appear: ${item}`);})}.margin({ left: 10, right: 10 })}}, (item: string) => item) // 生成唯一鍵值}.cachedCount(5) // 設置顯示區域外上下預加載布局}
}
渲染結果如圖
非首次渲染
當LazyForEach數據源發生變化,需要再次渲染時,開發者應根據數據源的變化情況調用listener對應的接口,通知LazyForEach做相應的更新。
局部代碼如下:
class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {return this.dataArray[index];}public pushData(data: string): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`);}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info(`appear: ${item}`);})}.margin({ left: 10, right: 10 })}.onClick(() => {// 點擊追加子組件this.data.pushData(`Hello ${this.data.totalCount()}`);})}, (item: string) => item)}.cachedCount(5)}
}
渲染結果如圖:
LazyForEach之前:先實現IDataSource接口
// 實現IDataSource接口,用于管理listener監聽,以及通知LazyForEach數據更新
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return this.originDataArray.length;}public getData(index: number): string {return this.originDataArray[index];}// 該方法為框架側調用,為LazyForEach組件向其數據源處添加listener監聽registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {console.info('add listener');this.listeners.push(listener);}}// 該方法為框架側調用,為對應的LazyForEach組件在數據源處去除listener監聽unregisterDataChangeListener(listener: DataChangeListener): void {const pos = this.listeners.indexOf(listener);if (pos >= 0) {console.info('remove listener');this.listeners.splice(pos, 1);}}// 通知LazyForEach組件需要重載所有子組件notifyDataReload(): void {this.listeners.forEach(listener => {listener.onDataReloaded();});}// 通知LazyForEach組件需要在index對應索引處添加子組件notifyDataAdd(index: number): void {this.listeners.forEach(listener => {listener.onDataAdd(index);// 寫法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);});}// 通知LazyForEach組件在index對應索引處數據有變化,需要重建該子組件notifyDataChange(index: number): void {this.listeners.forEach(listener => {listener.onDataChange(index);// 寫法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);});}// 通知LazyForEach組件需要在index對應索引處刪除該子組件notifyDataDelete(index: number): void {this.listeners.forEach(listener => {listener.onDataDelete(index);// 寫法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);});}// 通知LazyForEach組件將from索引和to索引處的子組件進行交換notifyDataMove(from: number, to: number): void {this.listeners.forEach(listener => {listener.onDataMove(from, to);// 寫法2:listener.onDatasetChange(// [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);});}notifyDatasetChange(operations: DataOperation[]): void {this.listeners.forEach(listener => {listener.onDatasetChange(operations);});}
}