• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 状态管理优秀实践
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @jiyujia926-->
5<!--Designer: @s10021109-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9本章节旨在帮助开发者提高ArkUI应用质量,重点提高状态管理效率。章节中列举了常见的低效开发场景及解决方案,并通过对比推荐与非推荐用法,帮助开发者正确使用状态变量,实现高性能开发。
10
11## 使用@ObjectLink代替@Prop减少不必要的深拷贝
12
13在应用开发中,父组件常向子组件传值。如果子组件不需要修改该状态变量,子组件使用@Prop装饰器会增加组件创建时间并影响性能,此时建议改用@ObjectLink。
14
15【反例】
16
17```ts
18@Observed
19class MyClass {
20  public num: number = 0;
21
22  constructor(num: number) {
23    this.num = num;
24  }
25}
26
27@Component
28struct PropChild {
29  @Prop testClass: MyClass; // @Prop装饰状态变量会深拷贝。
30
31  build() {
32    Text(`PropChild testNum ${this.testClass.num}`)
33  }
34}
35
36@Entry
37@Component
38struct Parent {
39  @State testClass: MyClass[] = [new MyClass(1)];
40
41  build() {
42    Column() {
43      Text(`Parent testNum ${this.testClass[0].num}`)
44        .onClick(() => {
45          this.testClass[0].num += 1;
46        })
47
48      // PropChild没有改变@Prop testClass: MyClass的值,所以这时最优的选择是使用@ObjectLink。
49      PropChild({ testClass: this.testClass[0] })
50    }
51  }
52}
53```
54
55在以上示例中,PropChild组件没有改变\@Prop testClass: MyClass的值,因此使用\@ObjectLink更为合适。因为@Prop会深拷贝数据带来性能开销,所以\@ObjectLink是比\@Link和\@Prop更优的选择。
56
57【正例】
58
59```ts
60@Observed
61class MyClass {
62  public num: number = 0;
63
64  constructor(num: number) {
65    this.num = num;
66  }
67}
68
69@Component
70struct PropChild {
71  @ObjectLink testClass: MyClass; // @ObjectLink装饰状态变量不会深拷贝。
72
73  build() {
74    Text(`PropChild testNum ${this.testClass.num}`)
75  }
76}
77
78@Entry
79@Component
80struct Parent {
81  @State testClass: MyClass[] = [new MyClass(1)];
82
83  build() {
84    Column() {
85      Text(`Parent testNum ${this.testClass[0].num}`)
86        .onClick(() => {
87          this.testClass[0].num += 1;
88        })
89
90      // 当子组件不需要本地修改状态时,应优先使用@ObjectLink,因为@Prop会执行深拷贝并带来性能开销,此时@ObjectLink是比@Link和@Prop更优的选择。
91      PropChild({ testClass: this.testClass[0] })
92    }
93  }
94}
95```
96
97
98## 不使用状态变量强行更新非状态变量关联组件
99
100【反例】
101
102
103```ts
104@Entry
105@Component
106struct MyComponent {
107  @State needsUpdate: boolean = true;
108  realStateArr: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器。
109  realState: Color = Color.Yellow;
110
111  updateUIArr(param: Array<number>): Array<number> {
112    const triggerAGet = this.needsUpdate;
113    return param;
114  }
115  updateUI(param: Color): Color {
116    const triggerAGet = this.needsUpdate;
117    return param;
118  }
119  build() {
120    Column({ space: 20 }) {
121      ForEach(this.updateUIArr(this.realStateArr),
122        (item: number) => {
123          Text(`${item}`)
124        })
125      Text("add item")
126        .onClick(() => {
127          // 改变realStateArr不会触发UI视图更新。
128          this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1);
129
130          // 触发UI视图更新。
131          this.needsUpdate = !this.needsUpdate;
132        })
133      Text("chg color")
134        .onClick(() => {
135          // 改变realState不会触发UI视图更新。
136          this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow;
137
138          // 触发UI视图更新。
139          this.needsUpdate = !this.needsUpdate;
140        })
141    }.backgroundColor(this.updateUI(this.realState))
142    .width(200).height(500)
143  }
144}
145```
146
147上述示例存在以下问题:
148
149- 应用程序希望控制UI更新逻辑,然而ArkUI的UI更新是由框架检测状态变量的变化触发的。
150
151- this.needsUpdate是一个自定义的状态变量,仅应用于其绑定的UI组件。变量this.realStateArrthis.realState没有被装饰,它们的变化不会触发UI刷新。
152
153- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realStateArrthis.realState的更新,这种方法不合理且会导致更新性能下降。
154
155【正例】
156
157解决此问题,需用\@State装饰realStateArr和realState成员变量。解决后就不再需要变量needsUpdate。
158
159```ts
160@Entry
161@Component
162struct CompA {
163  @State realStateArr: Array<number> = [4, 1, 3, 2];
164  @State realState: Color = Color.Yellow;
165  build() {
166    Column({ space: 20 }) {
167      ForEach(this.realStateArr,
168        (item: number) => {
169          Text(`${item}`)
170        })
171      Text("add item")
172        .onClick(() => {
173          // 改变realStateArr触发UI视图更新。
174          this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1);
175        })
176      Text("chg color")
177        .onClick(() => {
178          // 改变realState触发UI视图更新。
179          this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow;
180        })
181    }.backgroundColor(this.realState)
182    .width(200).height(500)
183  }
184}
185```
186
187## 精准控制状态变量关联的组件数
188
189建议每个状态变量关联的组件数少于20个。精准控制状态变量关联的组件数量可减少不必要的组件刷新,提升刷新效率。有时开发者会将同一状态变量绑定于多个同级组件属性,状态变化时将导致这些组件同步更新,产生不必要的刷新,当组件复杂度较高时会显著影响整体性能。相反,将该状态变量绑定在这些组件的父组件上,可以减少需要刷新的组件数,提高性能。
190
191【反例】
192
193```ts
194@Observed
195class Translate {
196  translateX: number = 20;
197}
198@Component
199struct Title {
200  @ObjectLink translateObj: Translate;
201  build() {
202    Row() {
203      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
204      Image($r('app.media.icon'))
205        .width(50)
206        .height(50)
207        .translate({
208          x:this.translateObj.translateX // this.translateObj.translateX 绑定在Image和Text组件上。
209        })
210      Text("Title")
211        .fontSize(20)
212        .translate({
213          x: this.translateObj.translateX
214        })
215    }
216  }
217}
218@Entry
219@Component
220struct Page {
221  @State translateObj: Translate = new Translate();
222
223  build() {
224    Column() {
225      Title({
226        translateObj: this.translateObj
227      })
228      Stack() {
229      }
230      .backgroundColor("black")
231      .width(200)
232      .height(400)
233      .translate({
234        x:this.translateObj.translateX //this.translateObj.translateX 绑定在Stack和Button组件上。
235      })
236      Button("move")
237        .translate({
238          x:this.translateObj.translateX
239        })
240        .onClick(() => {
241          this.getUIContext().animateTo({
242            duration: 50
243          },()=>{
244            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
245          })
246        })
247    }
248  }
249}
250```
251
252在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有子组件均位于Page组件的Column下,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。
253
254【正例】
255
256```ts
257@Observed
258class Translate {
259  translateX: number = 20;
260}
261@Component
262struct Title {
263  build() {
264    Row() {
265      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
266      Image($r('app.media.icon'))
267        .width(50)
268        .height(50)
269      Text("Title")
270        .fontSize(20)
271    }
272  }
273}
274@Entry
275@Component
276struct Page1 {
277  @State translateObj: Translate = new Translate();
278
279  build() {
280    Column() {
281      Title()
282      Stack() {
283      }
284      .backgroundColor("black")
285      .width(200)
286      .height(400)
287      Button("move")
288        .onClick(() => {
289          this.getUIContext().animateTo({
290            duration: 50
291          },()=>{
292            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
293          })
294        })
295    }
296    .translate({ // 子组件Stack和Button设置了同一个translate属性,可以统一到Column上设置。
297      x: this.translateObj.translateX
298    })
299  }
300}
301```
302
303## 合理控制对象类型状态变量关联的组件数量
304
305如果将一个复杂对象定义为状态变量,需要合理控制其关联的组件数。当对象中某个成员属性发生变化时,会导致该对象关联的所有组件刷新,即使这些组件并未直接使用该属性。为了避免这种“冗余刷新”对性能产生影响,建议合理拆分该复杂对象,控制对象关联的组件数量。具体可参考[精准控制组件的更新范围](https://gitcode.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/precisely-control-render-scope.md)和[状态管理合理使用开发指导](properly-use-state-management-to-develope.md)两篇文章。
306
307## 查询状态变量关联的组件数
308
309在应用开发中,可以通过HiDumper查看状态变量关联的组件数,进行性能优化。具体可参考[状态变量组件定位工具实践](https://gitcode.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/state_variable_dfx_pratice.md)310
311## 避免在for、while等循环逻辑中频繁读取状态变量
312
313在应用开发中,应避免在循环逻辑中频繁读取状态变量,而是应该放在循环外面读取。
314
315【反例】
316
317```ts
318import hilog from '@ohos.hilog';
319
320@Entry
321@Component
322struct Index {
323  @State message: string = '';
324
325  build() {
326    Column() {
327      Button('点击打印日志')
328        .onClick(() => {
329          for (let i = 0; i < 10; i++) {
330            hilog.info(0x0000, 'TAG', '%{public}s', this.message);
331          }
332        })
333        .width('90%')
334        .backgroundColor(Color.Blue)
335        .fontColor(Color.White)
336        .margin({
337          top: 10
338        })
339    }
340    .justifyContent(FlexAlign.Start)
341    .alignItems(HorizontalAlign.Center)
342    .margin({
343      top: 15
344    })
345  }
346}
347```
348
349【正例】
350
351```ts
352import hilog from '@ohos.hilog';
353
354@Entry
355@Component
356struct Index {
357  @State message: string = '';
358
359  build() {
360    Column() {
361      Button('点击打印日志')
362        .onClick(() => {
363          let logMessage: string = this.message;
364          for (let i = 0; i < 10; i++) {
365            hilog.info(0x0000, 'TAG', '%{public}s', logMessage);
366          }
367        })
368        .width('90%')
369        .backgroundColor(Color.Blue)
370        .fontColor(Color.White)
371        .margin({
372          top: 10
373        })
374    }
375    .justifyContent(FlexAlign.Start)
376    .alignItems(HorizontalAlign.Center)
377    .margin({
378      top: 15
379    })
380  }
381}
382```
383
384## 建议使用临时变量替换状态变量
385
386在应用开发中,应尽量减少对状态变量的直接赋值,通过临时变量完成数据计算操作。
387
388状态变量发生变化时,ArkUI会查询依赖该状态变量的组件并执行该组件的更新方法,完成组件渲染的行为。通过使用临时变量的计算代替直接操作状态变量,可以使ArkUI仅在最后一次状态变量变更时查询并渲染组件,减少不必要的操作,从而提高应用性能。状态变量行为可参考[@State装饰器:组件内状态](arkts-state.md)。
389
390【反例】
391
392```ts
393import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
394
395@Entry
396@Component
397struct Index {
398  @State message: string = '';
399
400  appendMsg(newMsg: string) {
401    // 性能打点
402    hiTraceMeter.startTrace('StateVariable', 1);
403    this.message += newMsg;
404    this.message += ';';
405    this.message += '<br/>';
406    hiTraceMeter.finishTrace('StateVariable', 1);
407  }
408
409  build() {
410    Column() {
411      Button('点击打印日志')
412        .onClick(() => {
413          this.appendMsg('操作状态变量');
414        })
415        .width('90%')
416        .backgroundColor(Color.Blue)
417        .fontColor(Color.White)
418        .margin({
419          top: 10
420        })
421    }
422    .justifyContent(FlexAlign.Start)
423    .alignItems(HorizontalAlign.Center)
424    .margin({
425      top: 15
426    })
427  }
428}
429```
430
431直接操作状态变量,三次触发计算函数,运行耗时结果如下:
432
433![](figures/hp_arkui_use_state_var.png)
434
435【正例】
436
437```ts
438import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
439
440@Entry
441@Component
442struct Index {
443  @State message: string = '';
444
445  appendMsg(newMsg: string) {
446    // 性能打点。
447    hiTraceMeter.startTrace('TemporaryVariable', 2);
448    let message = this.message;
449    message += newMsg;
450    message += ';';
451    message += '<br/>';
452    this.message = message;
453    hiTraceMeter.finishTrace('TemporaryVariable', 2);
454  }
455
456  build() {
457    Column() {
458      Button('点击打印日志')
459        .onClick(() => {
460          this.appendMsg('操作临时变量');
461        })
462        .width('90%')
463        .backgroundColor(Color.Blue)
464        .fontColor(Color.White)
465        .margin({
466          top: 10
467        })
468    }
469    .justifyContent(FlexAlign.Start)
470    .alignItems(HorizontalAlign.Center)
471    .margin({
472      top: 15
473    })
474  }
475}
476```
477
478使用临时变量取代状态变量的计算,三次触发计算函数,运行耗时结果如下:
479
480![](figures/hp_arkui_use_local_var.png)
481
482【总结】
483| **计算方式** | **耗时(局限不同设备和场景,数据仅供参考)**  | **说明** |
484| ------ | ------- | ------------------------------------- |
485| 直接操作状态变量  | 1.01ms | 增加了ArkUI不必要的查询和渲染行为,导致性能劣化。 |
486| 使用临时变量计算  | 0.63ms | 减少了ArkUI不必要的行为,优化性能。 |