鴻蒙HarmonyOS NEXT開發(fā):懶加載(LazyForEach)、瀑布流組件(WaterFlow)

一、簡介(LazyForEach)

懶加載LazyForEach是一種延遲加載的技術(shù),它是在需要的時候才加載數(shù)據(jù)或資源,并在每次迭代過程中創(chuàng)建相應(yīng)的組件,而不是一次性將所有內(nèi)容都加載出來。懶加載通常應(yīng)用于長列表、網(wǎng)格、瀑布流等數(shù)據(jù)量較大、子組件可重復(fù)使用的場景,當(dāng)用戶滾動頁面到相應(yīng)位置時,才會觸發(fā)資源的加載,以減少組件的加載時間,提高應(yīng)用性能,提升用戶體驗。

二、原理介紹

在聲明式描述語句中,有兩種方式控制列表、網(wǎng)格等容器類組件的渲染,分別為循環(huán)渲染(ForEach)和數(shù)據(jù)懶加載(LazyForEach)。

1、循環(huán)渲染(ForEach)

  • 從列表數(shù)據(jù)源一次性加載全量數(shù)據(jù)。

  • 為列表數(shù)據(jù)的每一個元素都創(chuàng)建對應(yīng)的組件,并全部掛載在組件樹上。即,F(xiàn)orEach遍歷多少個列表元素,就創(chuàng)建多少個ListItem組件節(jié)點(diǎn)并依次掛載在List組件樹根節(jié)點(diǎn)上。

  • 列表內(nèi)容顯示時,只渲染屏幕可視區(qū)內(nèi)的ListItem組件,可視區(qū)外的ListItem組件滑動進(jìn)入屏幕內(nèi)時,因為已經(jīng)完成了數(shù)據(jù)加載和組件創(chuàng)建掛載,直接渲染即可。

其數(shù)據(jù)加載、組件樹掛載、頁面渲染的示意圖如下所示:

image.png

如果列表數(shù)據(jù)較少,數(shù)據(jù)一次性全量加載不是性能瓶頸時,可以直接使用ForEach;但是當(dāng)數(shù)據(jù)量大、組件結(jié)構(gòu)復(fù)雜的情況下ForEach會出現(xiàn)性能瓶頸。這是因為要一次性加載所有的列表數(shù)據(jù),創(chuàng)建所有組件節(jié)點(diǎn)并完成組件樹的構(gòu)建,在數(shù)據(jù)量大時會非常耗時,從而導(dǎo)致頁面啟動時間過長。另外,屏幕可視區(qū)外的組件雖然不會顯示在屏幕上,但是仍然會占用內(nèi)存。在系統(tǒng)處于高負(fù)載的情況下,更容易出現(xiàn)性能問題,極限情況下甚至?xí)?dǎo)致應(yīng)用異常退出。

2、數(shù)據(jù)懶加載(LazyForEach)

  • LazyForEach會根據(jù)屏幕可視區(qū)能夠容納顯示的組件數(shù)量按需加載數(shù)據(jù)。

  • 根據(jù)加載的數(shù)據(jù)量創(chuàng)建組件,掛載在組件樹上,構(gòu)建出一棵短小的組件樹。即,屏幕可以展示多少列表項組件,就按需創(chuàng)建多少個ListItem組件節(jié)點(diǎn)掛載在List組件樹根節(jié)點(diǎn)上。

  • 屏幕可視區(qū)只展示部分組件。當(dāng)可視區(qū)外的組件需要在屏幕內(nèi)顯示時,需要從頭完成數(shù)據(jù)加載、組件創(chuàng)建、掛載組件樹這一過程,直至渲染到屏幕上。

其數(shù)據(jù)加載、組件樹掛載、頁面渲染的示意圖如下所示:

e55840f62c524ae8a618060799245502.jpg

LazyForEach實現(xiàn)了按需加載,針對列表數(shù)據(jù)量大、列表組件復(fù)雜的場景,減少了頁面首次啟動時一次性加載數(shù)據(jù)的時間消耗,減少了內(nèi)存峰值。不過在長列表滑動的過程中,因為需要根據(jù)用戶的滑動行為不斷地加載新的內(nèi)容,這需要進(jìn)行額外的數(shù)據(jù)請求和處理,會增加滑動時的計算量,從而對性能產(chǎn)生一定的影響。然而,合理使用LazyForEach的按需加載能力,通過在滑動停止或達(dá)到某個閾值時才進(jìn)行加載,可以減少不必要的計算和請求,從而提高性能,給用戶帶來更好的體驗。

三、使用方法(LazyForEach)

1、接口描述

LazyForEach(
    dataSource: IDataSource,             // 需要進(jìn)行數(shù)據(jù)迭代的數(shù)據(jù)源
    itemGenerator: (item: any, index: number) => void,  // 子組件生成函數(shù)
    keyGenerator?: (item: any, index: number) => string // 鍵值生成函數(shù)
): void

2、參數(shù)

參數(shù)名 參數(shù)類型 必填 參數(shù)描述
dataSource IDataSource LazyForEach數(shù)據(jù)源,需要開發(fā)者實現(xiàn)相關(guān)接口。
itemGenerator (item: any, index:number) => void 子組件生成函數(shù),為數(shù)組中的每一個數(shù)據(jù)項創(chuàng)建一個子組件。
keyGenerator (item: any, index:number) => string 鍵值生成函數(shù),用于給數(shù)據(jù)源中的每一個數(shù)據(jù)項生成唯一且固定的鍵值。

3、IDataSource類型說明

interface IDataSource {
    totalCount(): number; // 獲得數(shù)據(jù)總數(shù)
    getData(index: number): Object; // 獲取索引值對應(yīng)的數(shù)據(jù)
    registerDataChangeListener(listener: DataChangeListener): void; // 注冊數(shù)據(jù)改變的監(jiān)聽器
    unregisterDataChangeListener(listener: DataChangeListener): void; // 注銷數(shù)據(jù)改變的監(jiān)聽器
}

4、DataChangeListener類型說明

interface DataChangeListener {
    onDataReloaded(): void; // 重新加載數(shù)據(jù)完成后調(diào)用
    onDataAdded(index: number): void; // 添加數(shù)據(jù)完成后調(diào)用
    onDataMoved(from: number, to: number): void; // 數(shù)據(jù)移動起始位置與數(shù)據(jù)移動目標(biāo)位置交換完成后調(diào)用
    onDataDeleted(index: number): void; // 刪除數(shù)據(jù)完成后調(diào)用
    onDataChanged(index: number): void; // 改變數(shù)據(jù)完成后調(diào)用
    onDataAdd(index: number): void; // 添加數(shù)據(jù)完成后調(diào)用
    onDataMove(from: number, to: number): void; // 數(shù)據(jù)移動起始位置與數(shù)據(jù)移動目標(biāo)位置交換完成后調(diào)用
    onDataDelete(index: number): void; // 刪除數(shù)據(jù)完成后調(diào)用
    onDataChange(index: number): void; // 改變數(shù)據(jù)完成后調(diào)用
}

四、使用限制(LazyForEach)

  • LazyForEach必須在容器組件內(nèi)使用,僅有List、Grid、Swiper以及WaterFlow組件支持?jǐn)?shù)據(jù)懶加載(可配置cachedCount屬性,即只加載可視部分以及其前后少量數(shù)據(jù)用于緩沖),其他組件仍然是一次性加載所有的數(shù)據(jù)。

  • LazyForEach在每次迭代中,必須創(chuàng)建且只允許創(chuàng)建一個子組件。

  • 生成的子組件必須是允許包含在LazyForEach父容器組件中的子組件。

  • 允許LazyForEach包含在if/else條件渲染語句中,也允許LazyForEach中出現(xiàn)if/else條件渲染語句。

  • 鍵值生成器必須針對每個數(shù)據(jù)生成唯一的值,如果鍵值相同,將導(dǎo)致鍵值相同的UI組件渲染出現(xiàn)問題。

  • LazyForEach必須使用DataChangeListener對象來進(jìn)行更新,第一個參數(shù)dataSource使用狀態(tài)變量時,狀態(tài)變量改變不會觸發(fā)LazyForEach的UI刷新。

  • 為了高性能渲染,通過DataChangeListener對象的onDataChange方法來更新UI時,需要生成不同于原來的鍵值來觸發(fā)組件刷新。

五、使用場景(LazyForEach)

LazyForEach作為常見的渲染控制的方式之一,常用的使用場景有長列表加載、無限瀑布流等。

1、長列表加載

長列表作為應(yīng)用開發(fā)中最常見的開發(fā)場景之一,通常會包含成千上萬個列表項,在此場景下,直接使用循環(huán)渲染ForEach一次性加載所有的列表項,會導(dǎo)致渲染時間過長,影響用戶體驗。而使用數(shù)據(jù)懶加載LazyForEach替換循環(huán)渲染ForEach,可以按需加載列表項,從而提升列表性能。

雖然,按需加載列表項可以優(yōu)化長列表性能,但在快速滑動長列表的場景下,可能會來不及加載需要顯示的列表項,導(dǎo)致出現(xiàn)白塊的現(xiàn)象,從而影響用戶體驗。而在ArkUI中,List容器提供了cachedCount屬性,LazyForEach可以結(jié)合cachedCount屬性一起使用,能夠避免白塊的現(xiàn)象。cachedCount可以設(shè)置列表中ListItem/ListItemGroup的預(yù)加載數(shù)量,并且只在LazyForEach中生效,即cachedCount只能與LazyForEach一起使用。除了List容器,其他容器Grid、Swiper以及WaterFlow也都包含cachedCount屬性。cachedCount的使用方法如下所示。

List() {
  // ...
}.cachedCount(3)

2、無限瀑布流

瀑布流的內(nèi)容呈現(xiàn)方式類似瀑布流一樣,從上往下依次排列,每一列的高度不一定相同,整體呈現(xiàn)出瀑布流的視覺效果。在瀑布流中,經(jīng)常使用LazyForEach實現(xiàn)數(shù)據(jù)按需加載,同時,結(jié)合onReachEnd、onApear方法實現(xiàn)無限瀑布流。

雖然在onReachEnd()觸發(fā)時新增數(shù)據(jù)可以實現(xiàn)無限加載,但在滑動到底部時,會有明顯的停頓加載新數(shù)據(jù)的過程。

想要流暢的進(jìn)行無限滑動,還需要調(diào)整下增加新數(shù)據(jù)的時機(jī)。比如可以在LazyForEach還剩若干個數(shù)據(jù)就迭代到結(jié)束的情況下提前增加一些新數(shù)據(jù)。

六、瀑布流組件(WaterFlow)

瀑布流容器,由“行”和“列”分割的單元格所組成,通過容器自身的排列規(guī)則,將不同大小的“項目”自上而下,如瀑布般緊密布局。

1、接口

WaterFlow(options?: {footer?: CustomBuilder, scroller?: Scroller})

2、參數(shù)

參數(shù)名 參數(shù)類型 必填 參數(shù)描述
footer CustomBuilder 設(shè)置WaterFlow尾部組件。
scroller scroller 可滾動組件的控制器,與可滾動組件綁定。

3、屬性

名稱 參數(shù)類型 描述
columnsTemplate string 設(shè)置當(dāng)前瀑布流組件布局列的數(shù)量,不設(shè)置時默認(rèn)1列。
例如, '1fr 1fr 2fr' 是將父組件分3列,將父組件允許的寬分為4等份,第一列占1份,第二列占1份,第三列占2份。并支持auto-fill。
默認(rèn)值:'1fr'
rowsTemplate string 設(shè)置當(dāng)前瀑布流組件布局行的數(shù)量,不設(shè)置時默認(rèn)1行。
itemConstraintSize ConstraintSizeOptions 設(shè)置約束尺寸,子組件布局時,進(jìn)行尺寸范圍限制。
columnsGap Length 設(shè)置列與列的間距。
默認(rèn)值:0
rowsGap Length 設(shè)置行與行的間距。
默認(rèn)值:0
layoutDirection FlexDirection 設(shè)置布局的主軸方向。
默認(rèn)值:FlexDirection.Column

3、事件

名稱 功能描述
onReachStart(event: () => void) 瀑布流組件到達(dá)起始位置時觸發(fā)。
onReachEnd(event: () => void) 瀑布流組件到底末尾位置時觸發(fā)。

七、使用示例(無限瀑布流)

1、實現(xiàn)代碼

// WaterFlowDataSource.ets

// 實現(xiàn)IDataSource接口的對象,用于瀑布流組件加載數(shù)據(jù)
export class WaterFlowDataSource implements IDataSource {

  private dataArray: number[] = []
  private listeners: DataChangeListener[] = []

  // 獲取索引對應(yīng)的數(shù)據(jù)
  public getData(index: number): any {
    return this.dataArray[index]
  }

  // 獲取數(shù)據(jù)總數(shù)
  public totalCount(): number {
    return this.dataArray.length
  }

  // 注冊改變數(shù)據(jù)的控制器
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  // 注銷改變數(shù)據(jù)的控制器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  // 通知控制器數(shù)據(jù)增加
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  //增加數(shù)據(jù)
  public pushData(data: number): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

這里面只用到了添加方法,固其他方法省略,只保留基本的方法。

// WaterflowDemo.ets

import { WaterFlowDataSource } from './WaterFlowDataSource'

@Entry
@Component
struct WaterflowDemo {
  @State minSize: number = 80
  @State maxSize: number = 280
  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]
  datasource: WaterFlowDataSource = new WaterFlowDataSource()
  private itemWidthArray: number[] = []
  private itemHeightArray: number[] = []

  // 計算flow item寬/高
  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize)
    return (ret > this.minSize ? ret : this.minSize)
  }

  // 新增數(shù)據(jù),并保存flow item寬/高
  getItemSizeArray(t=0) {
    for (let i = 0; i < 20; i++) {
      this.itemWidthArray.push(this.getSize())
      this.itemHeightArray.push(this.getSize())
      this.datasource.pushData(i+t)
    }
  }

  aboutToAppear() {
    this.getItemSizeArray()
  }

  build() {
    Column() {
      WaterFlow() {
        LazyForEach(this.datasource, (item: number) => {
          FlowItem() {
            Column() {
              Text("N" + item).fontSize(14)
            }
          }
          .width(this.itemWidthArray[item])
          .height(this.itemHeightArray[item])
          .backgroundColor(this.colors[item % 5])
          // 此處通過在FlowItem的onAppear中判斷距離數(shù)據(jù)終點(diǎn)的數(shù)量,提前增加數(shù)據(jù)的方式實現(xiàn)了無停頓的無限滾動。
          .onAppear(() => {
            // 即將觸底時提前增加數(shù)據(jù)
            if (item + 20 == this.datasource.totalCount()) {
              this.getItemSizeArray(this.datasource.totalCount())
            }
          })
        }, item => item)
      }
      .columnsTemplate("1fr 1fr")
      .columnsGap(10)
      .rowsGap(10)
      // 瀑布流組件到達(dá)起始位置時觸發(fā)。
      .onReachStart(() => {
        console.info("onReachStart")
      })
      // 瀑布流組件到底末尾位置時觸發(fā)。
      .onReachEnd(() => {
        console.info("onReachEnd")
      })
      .backgroundColor(0xFAEEE0)
      .width('100%')
      .height('100%')
    }
  }
}

這里沒有圖片,用了隨機(jī)背景色代替。

2、實現(xiàn)效果

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容