一、簡介(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ù)加載、組件樹掛載、頁面渲染的示意圖如下所示:

如果列表數(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ù)加載、組件樹掛載、頁面渲染的示意圖如下所示:

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)效果
