• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 自定义组件冻结功能
2
3自定义组件冻结功能专为优化复杂UI页面的性能而设计,尤其适用于包含多个页面栈、长列表或宫格布局的场景。在这些情况下,当状态变量绑定了多个UI组件,其变化可能触发大量UI组件的刷新,进而导致界面卡顿和响应延迟。为了提升这类负载UI界面的刷新性能,开发者可以选择尝试使用自定义组件冻结功能。
4
5组件冻结的工作原理是:
61. 开发者通过设置freezeWhenInactive属性,即可激活组件冻结机制。
72. 启用后,系统将仅对处于激活状态的自定义组件进行更新,这使得UI框架可以尽量缩小更新范围,仅限于用户可见范围内(激活状态)的自定义组件,从而提高复杂UI场景下的刷新效率。
83. 当之前处于inactive状态的自定义组件重新变为active状态时,状态管理框架会对其执行必要的刷新操作,确保UI的正确展示。
9
10简而言之,组件冻结旨在优化复杂界面下的UI刷新性能。在存在多个不可见自定义组件的情况下,如多页面栈、长列表或宫格,通过组件冻结可以实现按需刷新,即仅刷新当前可见的自定义组件,而将不可见自定义组件的刷新延迟至它们变为可见时。
11
12需要注意,组件active/inactive并不等同于其可见性。组件冻结目前仅适用于以下场景:
13
141. 页面路由:当前栈顶页面为active,非栈顶不可见页面为inactive。
152. TabContent:只有当前显示的TabContent中的自定义组件处于active状态,其余则为inactive。
163. LazyForEach:仅当前显示的LazyForEach中的自定义组件为active,而缓存节点的组件则为inactive。
174. Navigation:当前显示的NavDestination中的自定义组件为active,而其他未显示的NavDestination组件则为inactive。
18其他场景,如堆叠布局(Stack)下的被遮罩的组件,这些组件尽管不可见,但并不被视为inactive状态,因此不在组件冻结的适用范围内。
19
20
21> **说明:**
22>
23> 从API version 11开始,支持自定义组件冻结功能。
24
25## 当前支持的场景
26
27### 页面路由
28
29- 当页面A调用router.pushUrl接口跳转到页面B时,页面A为隐藏不可见状态,此时如果更新页面A中的状态变量,不会触发页面A刷新。
30
31
32页面A:
33
34```ts
35import { router } from '@kit.ArkUI';
36
37@Entry
38@Component({ freezeWhenInactive: true })
39struct FirstTest {
40  @StorageLink('PropA') @Watch("first") storageLink: number = 47;
41
42  first() {
43    console.info("first page " + `${this.storageLink}`)
44  }
45
46  build() {
47    Column() {
48      Text(`From fist Page ${this.storageLink}`).fontSize(50)
49      Button('first page storageLink + 1').fontSize(30)
50        .onClick(() => {
51          this.storageLink += 1
52        })
53      Button('go to next page').fontSize(30)
54        .onClick(() => {
55          router.pushUrl({ url: 'pages/second' })
56        })
57    }
58  }
59}
60```
61
62页面B:
63
64```ts
65import { router } from '@kit.ArkUI';
66
67@Entry
68@Component({ freezeWhenInactive: true })
69struct SecondTest {
70  @StorageLink('PropA') @Watch("second") storageLink2: number = 1;
71
72  second() {
73    console.info("second page: " + `${this.storageLink2}`)
74  }
75
76  build() {
77    Column() {
78
79      Text(`second Page ${this.storageLink2}`).fontSize(50)
80      Button('Change Divider.strokeWidth')
81        .onClick(() => {
82          router.back()
83        })
84
85      Button('second page storageLink2 + 2').fontSize(30)
86        .onClick(() => {
87          this.storageLink2 += 2
88        })
89
90    }
91  }
92}
93```
94
95在上面的示例中:
96
971.点击页面A中的Button “first page storageLink + 1”,storageLink状态变量改变,@Watch中注册的方法first会被调用。
98
992.通过router.pushUrl({url: 'pages/second'}),跳转到页面B,页面A隐藏,状态由active变为inactive。
100
1013.点击页面B中的Button “this.storageLink2 += 2”,只回调页面B@Watch中注册的方法second,因为页面A的状态变量此时已被冻结。
102
1034.点击“back”,页面B被销毁,页面A的状态由inactive变为active,重新刷新在inactive时被冻结的状态变量,页面A@Watch中注册的方法first被再次调用。
104
105
106### TabContent
107
108- 对Tabs中当前不可见的TabContent进行冻结,不会触发组件的更新。
109
110- 需要注意的是:在首次渲染的时候,Tab只会创建当前正在显示的TabContent,当切换全部的TabContent后,TabContent才会被全部创建。
111
112```ts
113@Entry
114@Component
115struct TabContentTest {
116  @State @Watch("onMessageUpdated") message: number = 0;
117  private data: number[] = [0, 1]
118
119  onMessageUpdated() {
120    console.info(`TabContent message callback func ${this.message}`)
121  }
122
123  build() {
124    Row() {
125      Column() {
126        Button('change message').onClick(() => {
127          this.message++
128        })
129
130        Tabs() {
131          ForEach(this.data, (item: number) => {
132            TabContent() {
133              FreezeChild({ message: this.message, index: item })
134            }.tabBar(`tab${item}`)
135          }, (item: number) => item.toString())
136        }
137      }
138      .width('100%')
139    }
140    .height('100%')
141  }
142}
143
144@Component({ freezeWhenInactive: true })
145struct FreezeChild {
146  @Link @Watch("onMessageUpdated") message: number
147  private index: number = 0
148
149  onMessageUpdated() {
150    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
151  }
152
153  build() {
154    Text("message" + `${this.message}, index: ${this.index}`)
155      .fontSize(50)
156      .fontWeight(FontWeight.Bold)
157  }
158}
159```
160
161在上面的示例中:
162
1631.点击“change message”更改message的值,当前正在显示的TabContent组件中的@Watch中注册的方法onMessageUpdated被触发。
164
1652.点击“two”切换到另外的TabContent,TabContent状态由inactive变为active,对应的@Watch中注册的方法onMessageUpdated被触发。
166
1673.再次点击“change message”更改message的值,仅当前显示的TabContent子组件中的@Watch中注册的方法onMessageUpdated被触发。
168
169![TabContent.gif](figures/TabContent.gif)
170
171
172### LazyForEach
173
174- 对LazyForEach中缓存的自定义组件进行冻结,不会触发组件的更新。
175
176```ts
177// Basic implementation of IDataSource to handle data listener
178class BasicDataSource implements IDataSource {
179  private listeners: DataChangeListener[] = [];
180  private originDataArray: string[] = [];
181
182  public totalCount(): number {
183    return 0;
184  }
185
186  public getData(index: number): string {
187    return this.originDataArray[index];
188  }
189
190  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
191  registerDataChangeListener(listener: DataChangeListener): void {
192    if (this.listeners.indexOf(listener) < 0) {
193      console.info('add listener');
194      this.listeners.push(listener);
195    }
196  }
197
198  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
199  unregisterDataChangeListener(listener: DataChangeListener): void {
200    const pos = this.listeners.indexOf(listener);
201    if (pos >= 0) {
202      console.info('remove listener');
203      this.listeners.splice(pos, 1);
204    }
205  }
206
207  // 通知LazyForEach组件需要重载所有子组件
208  notifyDataReload(): void {
209    this.listeners.forEach(listener => {
210      listener.onDataReloaded();
211    })
212  }
213
214  // 通知LazyForEach组件需要在index对应索引处添加子组件
215  notifyDataAdd(index: number): void {
216    this.listeners.forEach(listener => {
217      listener.onDataAdd(index);
218    })
219  }
220
221  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
222  notifyDataChange(index: number): void {
223    this.listeners.forEach(listener => {
224      listener.onDataChange(index);
225    })
226  }
227
228  // 通知LazyForEach组件需要在index对应索引处删除该子组件
229  notifyDataDelete(index: number): void {
230    this.listeners.forEach(listener => {
231      listener.onDataDelete(index);
232    })
233  }
234}
235
236class MyDataSource extends BasicDataSource {
237  private dataArray: string[] = [];
238
239  public totalCount(): number {
240    return this.dataArray.length;
241  }
242
243  public getData(index: number): string {
244    return this.dataArray[index];
245  }
246
247  public addData(index: number, data: string): void {
248    this.dataArray.splice(index, 0, data);
249    this.notifyDataAdd(index);
250  }
251
252  public pushData(data: string): void {
253    this.dataArray.push(data);
254    this.notifyDataAdd(this.dataArray.length - 1);
255  }
256}
257
258@Entry
259@Component
260struct LforEachTest {
261  private data: MyDataSource = new MyDataSource();
262  @State @Watch("onMessageUpdated") message: number = 0;
263
264  onMessageUpdated() {
265    console.info(`LazyforEach message callback func ${this.message}`)
266  }
267
268  aboutToAppear() {
269    for (let i = 0; i <= 20; i++) {
270      this.data.pushData(`Hello ${i}`)
271    }
272  }
273
274  build() {
275    Column() {
276      Button('change message').onClick(() => {
277        this.message++
278      })
279      List({ space: 3 }) {
280        LazyForEach(this.data, (item: string) => {
281          ListItem() {
282            FreezeChild({ message: this.message, index: item })
283          }
284        }, (item: string) => item)
285      }.cachedCount(5).height(500)
286    }
287
288  }
289}
290
291@Component({ freezeWhenInactive: true })
292struct FreezeChild {
293  @Link @Watch("onMessageUpdated") message: number;
294  private index: string = "";
295
296  aboutToAppear() {
297    console.info(`FreezeChild aboutToAppear index: ${this.index}`)
298  }
299
300  onMessageUpdated() {
301    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
302  }
303
304  build() {
305    Text("message" + `${this.message}, index: ${this.index}`)
306      .width('90%')
307      .height(160)
308      .backgroundColor(0xAFEEEE)
309      .textAlign(TextAlign.Center)
310      .fontSize(30)
311      .fontWeight(FontWeight.Bold)
312  }
313}
314```
315
316在上面的示例中:
317
3181.点击“change message”更改message的值,当前正在显示的ListItem中的子组件@Watch中注册的方法onMessageUpdated被触发。缓存节点@Watch中注册的方法不会被触发。(如果不加组件冻结,当前正在显示的ListItem和cachecount缓存节点@Watch中注册的方法onMessageUpdated都会触发watch回调。)
319
3202.List区域外的ListItem滑动到List区域内,状态由inactive变为active,对应的@Watch中注册的方法onMessageUpdated被触发。
321
3223.再次点击“change message”更改message的值,仅有当前显示的ListItem中的子组件@Watch中注册的方法onMessageUpdated被触发。
323
324![FrezzeLazyforEach.gif](figures/FrezzeLazyforEach.gif)
325
326### Navigation
327
328- 当NavDestination不可见时,会对其子自定义组件设置成非激活态,不会触发组件的刷新。当返回该页面时,其子自定义组件重新恢复成激活态,触发@Watch回调进行刷新。
329
330- 在下面例子中,NavigationContentMsgStack会被设置成非激活态,将不再响应状态变量的变化,也不会触发组件刷新。
331
332```ts
333@Entry
334@Component
335struct MyNavigationTestStack {
336  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
337  @State @Watch("info") message: number = 0;
338  @State logNumber: number = 0;
339
340  info() {
341    console.info(`freeze-test MyNavigation message callback ${this.message}`);
342  }
343
344  @Builder
345  PageMap(name: string) {
346    if (name === 'pageOne') {
347      pageOneStack({ message: this.message, logNumber: this.logNumber })
348    } else if (name === 'pageTwo') {
349      pageTwoStack({ message: this.message, logNumber: this.logNumber })
350    } else if (name === 'pageThree') {
351      pageThreeStack({ message: this.message, logNumber: this.logNumber })
352    }
353  }
354
355  build() {
356    Column() {
357      Button('change message')
358        .onClick(() => {
359          this.message++;
360        })
361      Navigation(this.pageInfo) {
362        Column() {
363          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
364            .width('80%')
365            .height(40)
366            .margin(20)
367            .onClick(() => {
368              this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈
369            })
370        }
371      }.title('NavIndex')
372      .navDestination(this.PageMap)
373      .mode(NavigationMode.Stack)
374    }
375  }
376}
377
378@Component
379struct pageOneStack {
380  @Consume('pageInfo') pageInfo: NavPathStack;
381  @State index: number = 1;
382  @Link message: number;
383  @Link logNumber: number;
384
385  build() {
386    NavDestination() {
387      Column() {
388        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
389        Text("cur stack size:" + `${this.pageInfo.size()}`)
390          .fontSize(30)
391          .fontWeight(FontWeight.Bold)
392        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
393          .width('80%')
394          .height(40)
395          .margin(20)
396          .onClick(() => {
397            this.pageInfo.pushPathByName('pageTwo', null);
398          })
399        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
400          .width('80%')
401          .height(40)
402          .margin(20)
403          .onClick(() => {
404            this.pageInfo.pop();
405          })
406      }.width('100%').height('100%')
407    }.title('pageOne')
408    .onBackPressed(() => {
409      this.pageInfo.pop();
410      return true;
411    })
412  }
413}
414
415@Component
416struct pageTwoStack {
417  @Consume('pageInfo') pageInfo: NavPathStack;
418  @State index: number = 2;
419  @Link message: number;
420  @Link logNumber: number;
421
422  build() {
423    NavDestination() {
424      Column() {
425        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
426        Text("cur stack size:" + `${this.pageInfo.size()}`)
427          .fontSize(30)
428          .fontWeight(FontWeight.Bold)
429        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
430          .width('80%')
431          .height(40)
432          .margin(20)
433          .onClick(() => {
434            this.pageInfo.pushPathByName('pageThree', null);
435          })
436        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
437          .width('80%')
438          .height(40)
439          .margin(20)
440          .onClick(() => {
441            this.pageInfo.pop();
442          })
443      }.width('100%').height('100%')
444    }.title('pageTwo')
445    .onBackPressed(() => {
446      this.pageInfo.pop();
447      return true;
448    })
449  }
450}
451
452@Component
453struct pageThreeStack {
454  @Consume('pageInfo') pageInfo: NavPathStack;
455  @State index: number = 3;
456  @Link message: number;
457  @Link logNumber: number;
458
459  build() {
460    NavDestination() {
461      Column() {
462        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
463        Text("cur stack size:" + `${this.pageInfo.size()}`)
464          .fontSize(30)
465          .fontWeight(FontWeight.Bold)
466        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
467          .width('80%')
468          .height(40)
469          .margin(20)
470          .onClick(() => {
471            this.pageInfo.pushPathByName('pageOne', null);
472          })
473        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
474          .width('80%')
475          .height(40)
476          .margin(20)
477          .onClick(() => {
478            this.pageInfo.pop();
479          })
480      }.width('100%').height('100%')
481    }.title('pageThree')
482    .onBackPressed(() => {
483      this.pageInfo.pop();
484      return true;
485    })
486  }
487}
488
489@Component({ freezeWhenInactive: true })
490struct NavigationContentMsgStack {
491  @Link @Watch("info") message: number;
492  @Link index: number;
493  @Link logNumber: number;
494
495  info() {
496    console.info(`freeze-test NavigationContent message callback ${this.message}`);
497    console.info(`freeze-test ---- called by content ${this.index}`);
498    this.logNumber++;
499  }
500
501  build() {
502    Column() {
503      Text("msg:" + `${this.message}`)
504        .fontSize(30)
505        .fontWeight(FontWeight.Bold)
506      Text("log number:" + `${this.logNumber}`)
507        .fontSize(30)
508        .fontWeight(FontWeight.Bold)
509    }
510  }
511}
512```
513
514在上面的示例中:
515
5161.点击“change message”更改message的值,当前正在显示的MyNavigationTestStack组件中的@Watch中注册的方法info被触发。
517
5182.点击“Next Page”切换到PageOne,创建pageOneStack节点。
519
5203.再次点击“change message”更改message的值,仅pageOneStack中的NavigationContentMsgStack子组件中的@Watch中注册的方法info被触发。
521
5224.再次点击“Next Page”切换到PageTwo,创建pageTwoStack节点。
523
5245.再次点击“change message”更改message的值,仅pageTwoStack中的NavigationContentMsgStack子组件中的@Watch中注册的方法info被触发。
525
5266.再次点击“Next Page”切换到PageThree,创建pageThreeStack节点。
527
5287.再次点击“change message”更改message的值,仅pageThreeStack中的NavigationContentMsgStack子组件中的@Watch中注册的方法info被触发。
529
5308.点击“Back Page”回到PageTwo,此时,仅pageTwoStack中的NavigationContentMsgStack子组件中的@Watch中注册的方法info被触发。
531
5329.再次点击“Back Page”回到PageOne,此时,仅pageOneStack中的NavigationContentMsgStack子组件中的@Watch中注册的方法info被触发。
533
53410.再次点击“Back Page”回到初始页,此时,无任何触发。
535
536![navigation-freeze.gif](figures/navigation-freeze.gif)