概述
在滑動(dòng)場(chǎng)景下,常常會(huì)對(duì)同一類(lèi)自定義組件的實(shí)例進(jìn)行頻繁的創(chuàng)建與銷(xiāo)毀。此時(shí)可以考慮通過(guò)組件復(fù)用減少頻繁創(chuàng)建與銷(xiāo)毀的能耗。組件復(fù)用時(shí),可能存在許多影響組件復(fù)用效率的操作,本篇文章將重點(diǎn)介紹如何通過(guò)組件復(fù)用四板斧提升復(fù)用性能。
組件復(fù)用四板斧:
- 第一板斧,減少組件復(fù)用的嵌套層級(jí),如果在復(fù)用的自定義組件中再嵌套自定義組件,會(huì)存在節(jié)點(diǎn)構(gòu)造的開(kāi)銷(xiāo),且需要在每個(gè)嵌套的子組件中的aboutToReuse方法中實(shí)現(xiàn)數(shù)據(jù)的刷新,造成耗時(shí)。
- 第二板斧,優(yōu)化狀態(tài)管理,精準(zhǔn)控制組件刷新范圍,在復(fù)用的場(chǎng)景下,需要控制狀態(tài)變量的刷新范圍,避免擴(kuò)大刷新范圍,降低組件復(fù)用的效率。
- 第三板斧,復(fù)用組件嵌套結(jié)構(gòu)會(huì)變更的場(chǎng)景,使用reuseId標(biāo)記不同結(jié)構(gòu)的組件構(gòu)成,如:使用if else結(jié)構(gòu)來(lái)控制組件的創(chuàng)建,會(huì)造成組件樹(shù)結(jié)構(gòu)的大幅變動(dòng),降低組件復(fù)用的效率。需使用reuseId標(biāo)記不同的組件結(jié)構(gòu),提升復(fù)用性能。
- 第四板斧,不要使用函數(shù)/方法作為復(fù)用組件的入?yún)?/strong>,復(fù)用時(shí)會(huì)觸發(fā)組件的構(gòu)造,如果函數(shù)入?yún)⒅写嬖诤臅r(shí)操作,會(huì)影響復(fù)用性能。
組件復(fù)用原理機(jī)制

如上圖①中,ListItem N-1滑出可視區(qū)域即將銷(xiāo)毀時(shí),如果標(biāo)記了@Reusable,就會(huì)進(jìn)入這個(gè)自定義組件所在父組件的復(fù)用緩存區(qū)。需注意在自定義組件首次顯示時(shí),不會(huì)觸發(fā)組件復(fù)用。后續(xù)創(chuàng)建新組件節(jié)點(diǎn)時(shí),會(huì)復(fù)用緩存區(qū)中的節(jié)點(diǎn),節(jié)約組件重新創(chuàng)建的時(shí)間。尤其是該復(fù)用組件具有相同的布局結(jié)構(gòu),僅有某些數(shù)據(jù)差異時(shí),通過(guò)組件復(fù)用可以提高列表頁(yè)面的加載速度和響應(yīng)速度。
如上圖②中,復(fù)用緩存池是一個(gè)Map套Array的數(shù)據(jù)結(jié)構(gòu),以reuseId為key,具有相同reuseId的組件在同一個(gè)Array中。如未設(shè)置reuseId,則reuseId默認(rèn)是自定義組件的名字。
如上圖③中,發(fā)生復(fù)用行為時(shí),會(huì)自動(dòng)遞歸調(diào)用復(fù)用池中取出的自定義組件的aboutToReuse回調(diào),應(yīng)用可以在這個(gè)時(shí)候刷新數(shù)據(jù)。
第一板斧,減少組件復(fù)用的嵌套層級(jí)
在組件復(fù)用場(chǎng)景下,過(guò)深的自定義組件的嵌套會(huì)增加組件復(fù)用的使用難度,比如需要逐個(gè)實(shí)現(xiàn)所有嵌套組件中aboutToReuse回調(diào)實(shí)現(xiàn)數(shù)據(jù)更新;因此推薦優(yōu)先使用@Builder替代自定義組件,減少嵌套層級(jí),利于維護(hù)切能提升頁(yè)面加載速度。正反例如下:
反例:
@Entry
@Component
struct ReduceLevel {
private data: BasicDateSource = new BasicDateSource();
aboutToAppear(): void {
for (let index = 0; index < 30; index++) {
this.data.pushData(index.toString())
}
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
//反例 使用自定義組件
ComponentA({ desc: item })
}
}, (item: string) => item)
}
}
}
}
@Reusable
@Component
struct ComponentA {
@State desc: string = '';
aboutToReuse(params: ESObject): void {
this.desc = params.desc as string;
}
build() {
// 在復(fù)用組件中嵌套使用自定義組件
ComponentB({ desc: this.desc })
}
}
@Component
struct ComponentB {
@State desc: string = '';
// 嵌套的組件中也需要實(shí)現(xiàn)aboutToReuse來(lái)進(jìn)行UI的刷新
aboutToReuse(params: ESObject): void {
this.desc = params.desc as string;
}
build() {
Column() {
Text('子組件' + this.desc)
.fontSize(30)
.fontWeight(30)
}
}
}
上述反例的操作中,在復(fù)用的自定義組件中嵌套了新的自定義組件。ArkUI中使用自定義組件時(shí),在build階段將在在后端FrameNode樹(shù)創(chuàng)建一個(gè)相應(yīng)的CustomNode節(jié)點(diǎn),在渲染階段時(shí)也會(huì)創(chuàng)建對(duì)應(yīng)的RenderNode節(jié)點(diǎn)。會(huì)造成組件復(fù)用下,CustomNode創(chuàng)建和和RenderNod渲染e的耗時(shí)。且嵌套的自定義組件ComponentB,也需要實(shí)現(xiàn)aboutToReuse來(lái)進(jìn)行數(shù)據(jù)的刷新。
正例:
@Entry
@Component
struct ReduceLevel {
private data: BasicDateSource = new BasicDateSource();
aboutToAppear(): void {
for (let index = 0; index < 30; index++) {
this.data.pushData(index.toString())
}
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
// 正例
ChildComponent({ desc: item })
}
}, (item: string) => item)
}
}
}
}
// 正例 使用組件復(fù)用
@Reusable
@Component
struct ChildComponent {
@State desc: string = '';
aboutToReuse(params: Record<string, Object>): void {
this.desc = params.desc as string;
}
build() {
Column() {
// 使用@Builder,可以減少自定義組件創(chuàng)建和渲染的耗時(shí)
ChildComponentBuilder({ paramA: this.desc })
}
}
}
class Temp {
paramA: string = '';
}
@Builder
function ChildComponentBuilder($$: Temp) {
Column() {
// 此處使用`${}`來(lái)進(jìn)行按引用傳遞,讓@Builder感知到數(shù)據(jù)變化,進(jìn)行UI刷新
Text(子組件 + ${$$.paramA})
.fontSize(30)
.fontWeight(30)
}
}
上述正例的操作中,在復(fù)用的自定義組件中用@Builder來(lái)代替了自定義組件。避免了CustomNode節(jié)點(diǎn)創(chuàng)建和RenderNode渲染的耗時(shí)。
第二板斧,優(yōu)化狀態(tài)管理,精準(zhǔn)控制組件刷新范圍使用
1.使用attributeModifier精準(zhǔn)控制組件屬性的刷新,避免組件不必要的屬性刷新
復(fù)用場(chǎng)景常用在高頻的刷新場(chǎng)景,精準(zhǔn)控制組件的刷新范圍可以有效減少主線程渲染負(fù)載,提升滑動(dòng)性能。正反例如下:
反例:
@Entry
@Component
struct PreciseRefreshing {
@State mainContentData: VideoDataSource = new VideoDataSource(); // 視頻展示列表
build() {
Column() {
List() {
LazyForEach(this.mainContentData, (item: VideoDataType) => {
ListItem() {
MyComponent({ authorName: item.authorName, fontSize: item.fontWeight })
}
}, (item: VideoDataType) => item.desc + item.fontWeight)
}
}
}
}
@Reusable
@Component
export struct MyComponent {
...
@State fontSize: number = 0;
aboutToReuse(params: ESObject): void {
this.authorName = params.authorName;
this.fontSize = params.fontSize;
}
build() {
RelativeContainer() {
Text(this.videoDesc)
.textAlign(TextAlign.Center)
.fontStyle(FontStyle.Normal)
.fontColor(Color.Pink)
.id('videoName')
.margin({ left: 10 })
.fontWeight(30)
.alignRules({
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
})
// 此處使用屬性直接進(jìn)行刷新,會(huì)造成Text所有屬性都刷新
.fontSize(this.fontSize)
}
.width('100%')
.height(100)
}
}
上述反例的操作中,通過(guò)aboutToReuse對(duì)fontSize狀態(tài)變量更新,進(jìn)而導(dǎo)致組件的全部屬性進(jìn)行刷新,造成不必要的耗時(shí)??梢钥紤]對(duì)需要更新的組件的屬性,進(jìn)行精準(zhǔn)刷新,避免不必要的重繪和渲染。
正例:
export class MyTextModifier implements AttributeModifier<TextAttribute> {
private fontSize: number = 30;
constructor() {
}
setFontSize(instance: TextAttribute,fontSize: number) {
instance.fontSize = fontSize;
return this;
}
applyNormalAttribute(instance: TextAttribute): void {
instance.textAlign(TextAlign.Center)
instance.fontStyle(FontStyle.Normal)
instance.fontColor(Color.Pink)
instance.id('videoName')
instance.margin({ left: 10 })
instance.fontWeight(30)
instance.fontSize(10)
instance.alignRules({
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
})
}
}
@Entry
@Component
struct PreciseRefreshing {
@State mainContentData: VideoDataSource = new VideoDataSource(); // 視頻展示列表
build() {
Column() {
List() {
LazyForEach(this.mainContentData, (item: VideoDataType) => {
ListItem() {
MyComponent({... fontSize: item.fontWeight })
}
}, (item: VideoDataType) => item.desc + item.fontWeight)
}
}
}
}
@Reusable
@Component
export struct MyComponent {
...
@State fontSize: number = 0;
textModifier:MyTextModifier=new MyTextModifier();
aboutToReuse(params: ESObject): void {
...
this.fontSize = params.fontSize;
this.textModifier.setFontSize(this.textModifier,this.fontSize)
}
build() {
RelativeContainer() {
...
Text(this.videoDesc)
// 采用attributeModifier來(lái)對(duì)需要更新的fontSize屬性進(jìn)行精準(zhǔn)刷新,避免不必要的屬性刷新。
.attributeModifier(this.textModifier)
...
}
}
}
上述正例的操作中,通過(guò)attributeModifier屬性來(lái)對(duì)text組件需要刷新的fontSize屬性進(jìn)行精準(zhǔn)刷新,避免text其它不需要更改的屬性的刷新。
2.使用@Link/@ObjectLink替代@Prop減少深拷貝,提升組件創(chuàng)建速度
在父子組件數(shù)據(jù)同步時(shí),如果僅僅是需要父組件向子組件同步數(shù)據(jù),不存在修改子組件的數(shù)據(jù)變化不同步給父組件的需求。建議使用@Link/@ObjectLink替代@Prop,@Prop在裝飾變量時(shí)會(huì)進(jìn)行深拷貝,在拷貝的過(guò)程中除了基本類(lèi)型、Map、Set、Date、Array外,都會(huì)丟失類(lèi)型。正反例如下:
反例:
@Component
struct ChildComponent {
@Prop message: string;
build() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
}
@Entry
@Component
struct FatherComponent {
@State message: string = 'Hello World';
build() {
Column() {
ChildComponent({ message: this.message })
}
}
}
上述反例的操作中,父子組件之間的數(shù)據(jù)同步用了@Prop來(lái)進(jìn)行,每個(gè)@Prop裝飾的變量在初始化時(shí)都在本地拷貝了一份數(shù)據(jù)。會(huì)增加創(chuàng)建時(shí)間及內(nèi)存的消耗,造成性能問(wèn)題。
正例:
@Component
struct ChildComponent {
@Link message: string;
build() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
}
}
@Entry
@Component
struct FatherComponent {
@State message: string = 'Hello World';
build() {
Column() {
ChildComponent({ message: this.message })
}
.width('100%')
.height('100%')
}
}
上述正例的操作中,父子組件之間的數(shù)據(jù)同步用了@Link來(lái)進(jìn)行,子組件@Link包裝類(lèi)把當(dāng)前this指針注冊(cè)給父組件,會(huì)直接將父組件的數(shù)據(jù)同步給子組件,實(shí)現(xiàn)父子組件數(shù)據(jù)的雙向同步,降低子組件創(chuàng)建時(shí)間和內(nèi)存消耗。
第三板斧,復(fù)用組件嵌套結(jié)構(gòu)會(huì)變更的場(chǎng)景,使用reuseId標(biāo)記不同結(jié)構(gòu)的組件構(gòu)成
在自定義組件復(fù)用的場(chǎng)景中,如果使用if/else條件語(yǔ)句來(lái)控制布局的結(jié)構(gòu),會(huì)導(dǎo)致在不同邏輯創(chuàng)建不同布局結(jié)構(gòu)嵌套的組件,從而造成組件樹(shù)結(jié)構(gòu)的不同。此時(shí)我們應(yīng)該使用reuseId來(lái)區(qū)分不同結(jié)構(gòu)的組件,確保系統(tǒng)能夠根據(jù)reuseId緩存各種結(jié)構(gòu)的組件,提升復(fù)用性能。正反例如下:
反例:
@Entry
@Component
struct ReuseID {
...
build() {
Column() {
List({ scroller: this.scroller }) {
LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
ListItem() {
Button({ type: ButtonType.Normal }) {
Row() {
if (chatInfo['isPublicChat']) {
PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
} else {
ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
.onClick(() => {
const sessionType = (chatInfo as ChatSessionEntity).sessionType
autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
imLogic.chat.chatSort()
})
}
}.padding({ left: 16, right: 16 })
}
.type(ButtonType.Normal)
.width('100%')
.height('100%')
.backgroundColor('#fff')
.borderRadius(0)
}
.height(72)
.swipeAction({
end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
})
}, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
)
}
.cachedCount(3)
.backgroundColor('#fff')
.onScrollIndex(startIndex => {
this.listStartIndex = startIndex;
})
.width('100%')
.height('100%')
}
}
}
@Reusable
@Component
struct PublicChatItem {
...
aboutToReuse(params: ESObject): void {
this.chatInfo = params.chatInfo
}
build() {
...
}
}
@Reusable
@Component
struct ChatItem {
aboutToReuse(params: ESObject): void {
this.chatInfo = params.chatInfo
}
build() {
...
}
}
上述反例的操作中,通過(guò)if else來(lái)控制組件樹(shù)走不同的分支,分別復(fù)用PublicChatItem組件和ChatItem組件。導(dǎo)致更新if分支時(shí)仍然走刪除重創(chuàng)的邏輯。考慮采用根據(jù)不同的分支設(shè)置不同的reuseId來(lái)提高復(fù)用的性能。
正例:
@Entry
@Component
struct ReuseID {
...
build() {
Column() {
List({ scroller: this.scroller }) {
LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
ListItem() {
// 使用reuseId進(jìn)行組件復(fù)用的控制
InnerRecentChat({ chatInfo: chatInfo }).reuseId(this.lazyChatList.getReuseIdByIndex(index))
}
.height(72)
.swipeAction({
end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
})
}, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
)
}
.cachedCount(3)
.backgroundColor('#fff')
.onScrollIndex(startIndex => {
this.listStartIndex = startIndex;
})
.width('100%')
.height('100%')
}
}
}
@Reusable
@Component
struct InnerRecentChat {
...
aboutToReuse(params: ESObject): void {
this.chatInfo = params.chatInfo
}
build() {
Button({ type: ButtonType.Normal }) {
Row() {
if (this.chatInfo['isPublicChat']) {
PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
} else {
ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
.onClick(() => {
const sessionType = (chatInfo as ChatSessionEntity).sessionType
autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
imLogic.chat.chatSort()
})
}
}.padding({ left: 16, right: 16 })
}
.type(ButtonType.Normal)
.width('100%')
.height('100%')
.backgroundColor('#fff')
.borderRadius(0)
}
}
class MtDataSource extends BasicDataSource{
private chatList:Array<ChatSessionEntity|IChat.PublicChat>=[];
private reuseIds:Array<string>=[];
public totalCount():number{
return this.chatList.length;
}
public set (list:Array<ChatSessionEntity|IChat.PublicChat>){
this.chatList=list;
this.reuseIds=list.map((value:ChatSessionEntity|IChat.PublicChat)=>{
if (value['isPublicChat']) {
return "public";
}
else {
if ((value as ChatSessionEntity).target?.isEmployeeEntity()) {
return "employee"
}else {
return "group"
}
}
})
this.notifyDataReload();
}
pubilc getReuseIdByIndex(index:number):string{
return this.reuseIds
}
}
上述正例的操作中,通過(guò)reuseId來(lái)標(biāo)識(shí)需要復(fù)用的組件,省去走if else刪除重創(chuàng)的邏輯,提高組件復(fù)用的效率和性能。
第四板斧,避免使用函數(shù)/方法作為復(fù)用組件創(chuàng)建時(shí)的入?yún)?/h2>
由于在組件復(fù)用的場(chǎng)景下,每次復(fù)用都需要重新創(chuàng)建組件關(guān)聯(lián)的數(shù)據(jù)對(duì)象,導(dǎo)致重復(fù)執(zhí)行入?yún)⒅械暮瘮?shù)來(lái)獲取入?yún)⒔Y(jié)果。如果函數(shù)中存在耗時(shí)操作,會(huì)嚴(yán)重影響性能。正反例如下:
【反例】
// 下文中BasicDateSource是實(shí)現(xiàn)IDataSource接口的類(lèi),具體可參考LazyForEach用法指導(dǎo)
// 此處為復(fù)用的自定義組件
@Reusable
@Component
struct ChildComponent {
@State desc: string = '';
@State sum: number = 0;
aboutToReuse(params: Record<string, Object>): void {
this.desc = params.desc as string;
this.sum = params.sum as number;
}
build() {
Column() {
Text('子組件' + this.desc)
.fontSize(30)
.fontWeight(30)
Text('結(jié)果' + this.sum)
.fontSize(30)
.fontWeight(30)
}
}
}
@Entry
@Component
struct Reuse {
private data: BasicDateSource = new BasicDateSource();
aboutToAppear(): void {
for (let index = 0; index < 20; index++) {
this.data.pushData(index.toString())
}
}
// 真實(shí)場(chǎng)景的函數(shù)中可能存在未知的耗時(shí)操作邏輯,此處用循環(huán)函數(shù)模擬耗時(shí)操作
count(): number {
let temp: number = 0;
for (let index = 0; index < 10000; index++) {
temp += index;
}
return temp;
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
// 此處sum參數(shù)是函數(shù)獲取的,實(shí)際開(kāi)發(fā)場(chǎng)景無(wú)法預(yù)料該函數(shù)可能出現(xiàn)的耗時(shí)操作,每次進(jìn)行組件復(fù)用都會(huì)重復(fù)觸發(fā)此函數(shù)的調(diào)用
ChildComponent({ desc: item, sum: this.count() })
}
.width('100%')
.height(100)
}, (item: string) => item)
}
}
}
}
上述反例的操作中,復(fù)用的子組件參數(shù)sum是通過(guò)耗時(shí)函數(shù)生成。該函數(shù)在每次組件復(fù)用時(shí)都需要執(zhí)行,會(huì)造成性能問(wèn)題,甚至是列表滑動(dòng)過(guò)程中的卡頓丟幀現(xiàn)象。
【正例】
// 下文中BasicDateSource是實(shí)現(xiàn)IDataSource接口的類(lèi),具體可參考LazyForEach用法指導(dǎo)
// 此處為復(fù)用的自定義組件
@Reusable
@Component
struct ChildComponent {
@State desc: string = '';
@State sum: number = 0;
aboutToReuse(params: Record<string, Object>): void {
this.desc = params.desc as string;
this.sum = params.sum as number;
}
build() {
Column() {
Text('子組件' + this.desc)
.fontSize(30)
.fontWeight(30)
Text('結(jié)果' + this.sum)
.fontSize(30)
.fontWeight(30)
}
}
}
@Entry
@Component
struct Reuse {
private data: BasicDateSource = new BasicDateSource();
@State sum: number = 0;
aboutToAppear(): void {
for (let index = 0; index < 20; index++) {
this.data.pushData(index.toString())
}
// 執(zhí)行該異步函數(shù)
this.count();
}
// 模擬耗時(shí)操作邏輯
async count() {
let temp: number = 0;
for (let index = 0; index < 10000; index++) {
temp += index;
}
// 將結(jié)果放入狀態(tài)變量中
this.sum = temp;
}
build() {
Column() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
// 子組件的傳參通過(guò)狀態(tài)變量進(jìn)行
ChildComponent({ desc: item, sum: this.sum })
}
.width('100%')
.height(100)
}, (item: string) => item)
}
}
}
}
上述正例的操作中,通過(guò)耗時(shí)函數(shù)count生成的結(jié)果不變,可以將其放到頁(yè)面初始渲染時(shí)執(zhí)行一次,將結(jié)果賦值給this.sum。在復(fù)用組件的參數(shù)傳遞時(shí),通過(guò)this.sum來(lái)進(jìn)行。
寫(xiě)在最后
如果你覺(jué)得這篇內(nèi)容對(duì)你還蠻有幫助,我想邀請(qǐng)你幫我三個(gè)小忙:
- 點(diǎn)贊,轉(zhuǎn)發(fā),有你們的 『點(diǎn)贊和評(píng)論』,才是我創(chuàng)造的動(dòng)力。
- 關(guān)注小編,同時(shí)可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識(shí)。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識(shí)點(diǎn),請(qǐng)移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu
