• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# \@State装饰器:组件内状态
2
3
4\@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就可以触发其直接绑定UI组件的刷新。当状态改变时,UI会发生对应的渲染改变。
5
6
7在状态变量相关装饰器中,\@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。
8
9在阅读\@State文档前,建议开发者对状态管理框架有基本的了解。建议提前阅读:[状态管理概述](./arkts-state-management-overview.md)。
10
11> **说明:**
12>
13> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。
14>
15> 从API version 11开始,该装饰器支持在原子化服务中使用。
16
17## 概述
18
19\@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。
20
21\@State装饰的变量拥有以下特点:
22
23- \@State装饰的变量与子组件中的\@Prop装饰变量之间建立单向数据同步,与\@Link、\@ObjectLink装饰变量之间建立双向数据同步。
24
25- \@State装饰的变量生命周期与其所属自定义组件的生命周期相同。
26
27
28## 装饰器使用规则说明
29
30| \@State变量装饰器  | 说明                                                         |
31| ------------------ | ------------------------------------------------------------ |
32| 装饰器参数         | 无                                                           |
33| 同步类型           | 不与父组件中任何类型的变量同步。                             |
34| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>支持Date类型。<br/>API11及以上支持[Map](#装饰map类型变量)、[Set](#装饰set类型变量)类型。<br/>支持undefined和null类型。<br/>支持ArkUI框架定义的联合类型[Length](../../reference/apis-arkui/arkui-ts/ts-types.md#length)、[ResourceStr](../../reference/apis-arkui/arkui-ts/ts-types.md#resourcestr)、[ResourceColor](../../reference/apis-arkui/arkui-ts/ts-types.md#resourcecolor)类型。 <br/>类型必须被指定。<br/>支持类型的场景请参考[观察变化](#观察变化)。<br/>不支持any。<br/>API11及以上支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[@State支持联合类型实例](#state支持联合类型实例)。 <br/>**注意**<br/>当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:`@State a : string \| undefined = undefined`是支持的,不支持`@State a: string = undefined`。|
35| 被装饰变量的初始值 | 必须本地初始化。                                               |
36
37
38## 变量的传递/访问规则说明
39
40| 传递/访问          | 说明                                                         |
41| ------------------ | ------------------------------------------------------------ |
42| 从父组件初始化     | 可选,从父组件初始化或者本地初始化。如果从父组件初始化,并且从父组件传入的值非undefined,将会覆盖本地初始化;如果从父组件传入的值为undefined,则初值为@State装饰变量自身的初值。<br/>支持父组件中常规变量(常规变量对@State赋值,只是数值的初始化,常规变量的变化不会触发UI刷新,只有状态变量才能触发UI刷新)、\@State、[\@Link](arkts-link.md)、[\@Prop](arkts-prop.md)、[\@Provide](arkts-provide-and-consume.md)、[\@Consume](arkts-provide-and-consume.md)、[\@ObjectLink](arkts-observed-and-objectlink.md)、[\@StorageLink](arkts-appstorage.md#storagelink)、[\@StorageProp](arkts-appstorage.md#storageprop)、[\@LocalStorageLink](arkts-localstorage.md#localstoragelink)和[\@LocalStorageProp](arkts-localstorage.md#localstorageprop)装饰的变量,初始化子组件的\@State。 |
43| 用于初始化子组件   | \@State装饰的变量支持初始化子组件的常规变量、\@State、\@Link、\@Prop、\@Provide。 |
44| 是否支持组件外访问 | 不支持,只能在组件内访问。                                   |
45
46  **图1** 初始化规则图示  
47
48![zh-cn_image_0000001502091796](figures/zh-cn_image_0000001502091796.png)
49
50
51## 观察变化和行为表现
52
53并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。本小节将介绍什么样的修改才能被观察到,以及观察到变化后,框架是怎么引起UI刷新的,即框架的行为表现是什么。
54
55
56### 观察变化
57
58- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
59
60  ```ts
61  // 简单类型
62  @State count: number = 0;
63  // 可以观察到值的变化
64  this.count = 1;
65  ```
66
67- 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。
68
69  声明Person和Model类。
70
71  ```ts
72  class Person {
73    public value: string;
74
75    constructor(value: string) {
76      this.value = value;
77    }
78  }
79
80  class Model {
81    public value: string;
82    public name: Person;
83    constructor(value: string, person: Person) {
84      this.value = value;
85      this.name = person;
86    }
87  }
88  ```
89
90  \@State装饰的类型是Model
91
92  ```ts
93  // class类型
94  @State title: Model = new Model('Hello', new Person('World'));
95  ```
96
97  对\@State装饰变量的赋值。
98
99  ```ts
100  // class类型赋值
101  this.title = new Model('Hi', new Person('ArkUI'));
102  ```
103
104  对\@State装饰变量的属性赋值。
105
106  ```ts
107  // class属性的赋值
108  this.title.value = 'Hi';
109  ```
110
111  嵌套属性的赋值观察不到。
112
113  ```ts
114  // 嵌套的属性赋值观察不到
115  this.title.name.value = 'ArkUI';
116  ```
117- 当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。例子如下。
118  声明Model类。
119
120  ```ts
121  class Model {
122    public value: number;
123    constructor(value: number) {
124      this.value = value;
125    }
126  }
127  ```
128
129  \@State装饰的对象为Model类型数组时。
130
131  ```ts
132  // 数组类型
133  @State title: Model[] = [new Model(11), new Model(1)];
134  ```
135
136  数组自身的赋值可以观察到。
137
138  ```ts
139  // 数组赋值
140  this.title = [new Model(2)];
141  ```
142
143  数组项的赋值可以观察到。
144
145  ```ts
146  // 数组项赋值
147  this.title[0] = new Model(2);
148  ```
149
150  删除数组项可以观察到。
151
152  ```ts
153  // 数组项更改
154  this.title.pop();
155  ```
156
157  新增数组项可以观察到。
158
159  ```ts
160  // 数组项更改
161  this.title.push(new Model(12));
162  ```
163
164  数组项中属性的赋值观察不到。
165
166  ```ts
167  // 嵌套的属性赋值观察不到
168  this.title[0].value = 6;
169  ```
170
171- 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。
172
173  ```ts
174  @Entry
175  @Component
176  struct DatePickerExample {
177    @State selectedDate: Date = new Date('2021-08-08');
178
179    build() {
180      Column() {
181        Button('set selectedDate to 2023-07-08')
182          .margin(10)
183          .onClick(() => {
184            this.selectedDate = new Date('2023-07-08');
185          })
186        Button('increase the year by 1')
187          .margin(10)
188          .onClick(() => {
189            this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
190          })
191        Button('increase the month by 1')
192          .margin(10)
193          .onClick(() => {
194            this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
195          })
196        Button('increase the day by 1')
197          .margin(10)
198          .onClick(() => {
199            this.selectedDate.setDate(this.selectedDate.getDate() + 1);
200          })
201        DatePicker({
202          start: new Date('1970-1-1'),
203          end: new Date('2100-1-1'),
204          selected: this.selectedDate
205        })
206      }.width('100%')
207    }
208  }
209  ```
210
211- 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口`set`, `clear`, `delete` 更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。
212
213- 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口`add`, `clear`, `delete` 更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。
214
215### 框架行为
216
217- 当状态变量被改变时,查询依赖该状态变量的组件;
218
219- 执行依赖该状态变量的组件的更新方法,组件更新渲染;
220
221- 和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。
222
223
224## 限制条件
225
2261. \@State装饰的变量必须初始化,否则编译期会报错。
227
228  ```ts
229  // 错误写法,编译报错
230  @State count: number;
231
232  // 正确写法
233  @State count: number = 10;
234  ```
235
2362. \@State不支持装饰Function类型的变量,框架会抛出运行时错误。
237
238
239## 使用场景
240
241
242### 装饰简单类型的变量
243
244以下示例为\@State装饰的简单类型,count被\@State装饰成为状态变量,count的改变引起Button组件的刷新:
245
246- 当状态变量count改变时,查询到只有Button组件关联了它;
247
248- 执行Button组件的更新方法,实现按需刷新。
249
250
251```ts
252@Entry
253@Component
254struct MyComponent {
255  @State count: number = 0;
256
257  build() {
258    Button(`click times: ${this.count}`)
259      .onClick(() => {
260        this.count += 1;
261      })
262  }
263}
264```
265
266
267### 装饰class对象类型的变量
268
269- 自定义组件MyComponent定义了被\@State装饰的状态变量count和title,其中title的类型为自定义类Model。如果count或title的值发生变化,则查询MyComponent中使用该状态变量的UI组件,并进行重新渲染。
270
271- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent。
272
273```ts
274class Model {
275  public value: string;
276
277  constructor(value: string) {
278    this.value = value;
279  }
280}
281
282@Entry
283@Component
284struct EntryComponent {
285  build() {
286    Column() {
287      // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化
288      MyComponent({ count: 1, increaseBy: 2 })
289        .width(300)
290      MyComponent({ title: new Model('Hello World 2'), count: 7 })
291    }
292  }
293}
294
295@Component
296struct MyComponent {
297  @State title: Model = new Model('Hello World');
298  @State count: number = 0;
299  private increaseBy: number = 1;
300
301  build() {
302    Column() {
303      Text(`${this.title.value}`)
304        .margin(10)
305      Button(`Click to change title`)
306        .onClick(() => {
307          // @State变量的更新将触发上面的Text组件内容更新
308          this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI';
309        })
310        .width(300)
311        .margin(10)
312
313      Button(`Click to increase count = ${this.count}`)
314        .onClick(() => {
315          // @State变量的更新将触发该Button组件的内容更新
316          this.count += this.increaseBy;
317        })
318        .width(300)
319        .margin(10)
320    }
321  }
322}
323```
324
325![Video-state](figures/Video-state.gif)
326
327从该示例中,我们可以了解到\@State变量的初始化机制:
328
329
3301. 没有外部传入的情况下,使用默认的值进行本地初始化:
331
332   ```ts
333   // title没有外部传入,使用本地的值new Model('Hello World')进行初始化
334   MyComponent({ count: 1, increaseBy: 2 })
335   // increaseBy没有外部传入,使用本地的值1进行初始化
336   MyComponent({ title: new Model('Hello World 2'), count: 7 })
337   ```
338
3392. 有外部传入的情况下,使用外部传入的值进行初始化:
340
341   ```ts
342   // count和increaseBy均有外部传入,分别使用传入的1和2进行初始化
343   MyComponent({ count: 1, increaseBy: 2 })
344   // title和count均有外部传入,分别使用传入的new Model('Hello World 2')和7进行初始化
345   MyComponent({ title: new Model('Hello World 2'), count: 7 })
346   ```
347
348
349### 装饰Map类型变量
350
351> **说明:**
352>
353> 从API version 11开始,\@State支持Map类型。
354
355在下面的示例中,message类型为Map\<number, string\>,点击Button改变message的值,视图会随之刷新。
356
357```ts
358@Entry
359@Component
360struct MapSample {
361  @State message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);
362
363  build() {
364    Row() {
365      Column() {
366        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
367          Text(`${item[0]}`).fontSize(30)
368          Text(`${item[1]}`).fontSize(30)
369          Divider()
370        })
371        Button('init map').onClick(() => {
372          this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]);
373        })
374        Button('set new one').onClick(() => {
375          this.message.set(4, "d");
376        })
377        Button('clear').onClick(() => {
378          this.message.clear();
379        })
380        Button('replace the first one').onClick(() => {
381          this.message.set(0, "aa");
382        })
383        Button('delete the first one').onClick(() => {
384          this.message.delete(0);
385        })
386      }
387      .width('100%')
388    }
389    .height('100%')
390  }
391}
392```
393
394### 装饰Set类型变量
395
396> **说明:**
397>
398> 从API version 11开始,\@State支持Set类型。
399
400在下面的示例中,message类型为Set\<number\>,点击Button改变message的值,视图会随之刷新。
401
402```ts
403@Entry
404@Component
405struct SetSample {
406  @State message: Set<number> = new Set([0, 1, 2, 3, 4]);
407
408  build() {
409    Row() {
410      Column() {
411        ForEach(Array.from(this.message.entries()), (item: [number]) => {
412          Text(`${item[0]}`).fontSize(30)
413          Divider()
414        })
415        Button('init set').onClick(() => {
416          this.message = new Set([0, 1, 2, 3, 4]);
417        })
418        Button('set new one').onClick(() => {
419          this.message.add(5);
420        })
421        Button('clear').onClick(() => {
422          this.message.clear();
423        })
424        Button('delete the first one').onClick(() => {
425          this.message.delete(0);
426        })
427      }
428      .width('100%')
429    }
430    .height('100%')
431  }
432}
433```
434
435## State支持联合类型实例
436
437\@State支持联合类型和undefined和null,在下面的示例中,count类型为number | undefined,点击Button改变count的属性或者类型,视图会随之刷新。
438
439```ts
440@Entry
441@Component
442struct EntryComponent {
443  build() {
444    Column() {
445      MyComponent()
446    }
447  }
448}
449
450@Component
451struct MyComponent {
452  @State count: number | undefined = 0;
453
454  build() {
455    Column() {
456      Text(`count(${this.count})`)
457      Button('change')
458        .onClick(() => {
459          this.count = undefined;
460        })
461    }
462  }
463}
464```
465
466
467## 常见问题
468
469### 使用箭头函数改变状态变量未生效
470
471箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。所以在该场景下, changeCoverUrl的this指向PlayDetailViewModel,而不是被装饰器\@State代理的状态变量。
472
473反例:
474
475```ts
476
477export default class PlayDetailViewModel {
478  coverUrl: string = '#00ff00';
479
480  changeCoverUrl= ()=> {
481    this.coverUrl = '#00F5FF';
482  }
483
484}
485```
486
487```ts
488import PlayDetailViewModel from './PlayDetailViewModel';
489
490@Entry
491@Component
492struct PlayDetailPage {
493  @State vm: PlayDetailViewModel = new PlayDetailViewModel();
494
495  build() {
496    Stack() {
497      Text(this.vm.coverUrl).width(100).height(100).backgroundColor(this.vm.coverUrl)
498      Row() {
499        Button('点击改变颜色')
500          .onClick(() => {
501            this.vm.changeCoverUrl();
502          })
503      }
504    }
505    .width('100%')
506    .height('100%')
507    .alignContent(Alignment.Top)
508  }
509}
510```
511
512所以要将当前this.vm传入,调用代理状态变量的属性赋值。
513
514正例:
515
516```ts
517
518export default class PlayDetailViewModel {
519  coverUrl: string = '#00ff00';
520
521  changeCoverUrl= (model:PlayDetailViewModel)=> {
522    model.coverUrl = '#00F5FF';
523  }
524
525}
526```
527
528```ts
529import PlayDetailViewModel from './PlayDetailViewModel';
530
531@Entry
532@Component
533struct PlayDetailPage {
534  @State vm: PlayDetailViewModel = new PlayDetailViewModel();
535
536  build() {
537    Stack() {
538      Text(this.vm.coverUrl).width(100).height(100).backgroundColor(this.vm.coverUrl)
539      Row() {
540        Button('点击改变颜色')
541          .onClick(() => {
542            let self = this.vm;
543            this.vm.changeCoverUrl(self);
544          })
545      }
546    }
547    .width('100%')
548    .height('100%')
549    .alignContent(Alignment.Top)
550  }
551}
552```
553
554### 类的构造函数中通过捕获this修改变量无法观察
555
556在状态管理中,类会被一层“代理”进行包装。当在组件中改变该类的成员变量时,会被该代理进行拦截,在更改数据源中值的同时,也会将变化通知给绑定的组件,从而实现观测变化与触发刷新。
557
558当开发者把修改success的箭头函数放在构造函数中初始化时,此时TestModel实例还未被代理封装,this指向TestModel实例本身,所以后续触发query事件无法被状态管理观测到变化。
559
560当开发者把修改success的箭头函数放在query中时,此时已完成TestModel对象初始化和代理封装。通过`this.viewModel.query()`方式调用query时,query函数中的this指向viewModel代理对象,对代理对象成员属性isSuccess的更改能够被观测到,因此触发query事件可以被状态管理观测到变化。
561
562【反例】
563
564```ts
565@Entry
566@Component
567struct Index {
568  @State viewModel: TestModel = new TestModel();
569
570  build() {
571    Row() {
572      Column() {
573        Text(this.viewModel.isSuccess ? 'success' : 'failed')
574          .fontSize(50)
575          .fontWeight(FontWeight.Bold)
576          .onClick(() => {
577            this.viewModel.query();
578          })
579      }.width('100%')
580    }.height('100%')
581  }
582}
583
584export class TestModel {
585  isSuccess: boolean = false;
586  model: Model
587
588  constructor() {
589    this.model = new Model(() => {
590      this.isSuccess = true;
591      console.log(`this.isSuccess: ${this.isSuccess}`);
592    })
593  }
594
595  query() {
596    this.model.query();
597  }
598}
599
600export class Model {
601  callback: () => void
602
603  constructor(cb: () => void) {
604    this.callback = cb;
605  }
606
607  query() {
608    this.callback();
609  }
610}
611```
612
613上文示例代码将状态变量的修改放在构造函数内,界面开始时显示“failed”,点击后日志打印“this.isSuccess: true”说明修改成功,但界面依旧显示“failed”,未实现刷新。
614
615【正例】
616
617```ts
618@Entry
619@Component
620struct Index {
621  @State viewModel: TestModel = new TestModel();
622
623  build() {
624    Row() {
625      Column() {
626        Text(this.viewModel.isSuccess ? 'success' : 'failed')
627          .fontSize(50)
628          .fontWeight(FontWeight.Bold)
629          .onClick(() => {
630            this.viewModel.query();
631          })
632      }.width('100%')
633    }.height('100%')
634  }
635}
636
637export class TestModel {
638  isSuccess: boolean = false;
639  model: Model = new Model(() => {
640  })
641
642  query() {
643    this.model.callback = () => {
644      this.isSuccess = true;
645    }
646    this.model.query();
647  }
648}
649
650export class Model {
651  callback: () => void
652
653  constructor(cb: () => void) {
654    this.callback = cb;
655  }
656
657  query() {
658    this.callback();
659  }
660}
661```
662
663上文示例代码将状态变量的修改放在类的普通方法中,界面开始时显示“failed”,点击后显示“success”。
664
665### 状态变量只能影响其直接绑定的UI组件的刷新
666
667【示例1】
668
669```ts
670class Info {
671  address: string = '杭州';
672}
673
674@Entry
675@Component
676struct Test {
677  @State message: string = '上海';
678  @State info: Info = new Info();
679
680  aboutToAppear(): void {
681    this.info.address = this.message;
682  }
683
684  build() {
685    Column() {
686      Text(`${this.message}`);
687      Text(`${this.info.address}`);
688      Button('change')
689        .onClick(() => {
690          this.info.address = '北京';
691        })
692    }
693  }
694}
695```
696
697以上示例点击Button('change'),只会触发第二个Text组件的刷新,因为message是简单类型string,简单类型是值拷贝,所以点击按钮改变的是info中的address值,不会影响this.message的值。
698
699【示例2】
700
701```ts
702class Info {
703  address: string = '杭州';
704
705  constructor(address: string) {
706    this.address = address;
707  }
708}
709
710class User {
711  info: Info = new Info('天津');
712}
713
714@Entry
715@Component
716struct Test {
717  @State info: Info = new Info('上海');
718  @State user: User = new User();
719
720  aboutToAppear(): void {
721    this.user.info = this.info;
722  }
723
724  build() {
725    Column() {
726      Text(`${this.info.address}`);
727      Text(`${this.user.info.address}`);
728      Button('change')
729        .onClick(() => {
730          this.user.info.address = '北京';
731        })
732    }
733  }
734}
735```
736
737在上述示例中,由于在aboutToAppear中将info的引用赋值给了user的成员属性info,因此点击按钮改变info中的属性时,会触发第一个Text组件的刷新。而第二个Text组件因为观测能力仅有一层,无法观测到二层属性的变化,所以不会刷新。
738
739【示例3】
740
741```ts
742class Info {
743  address: string = '杭州';
744
745  constructor(address: string) {
746    this.address = address;
747  }
748}
749
750class User {
751  info: Info = new Info('天津');
752}
753
754@Entry
755@Component
756struct Test {
757  @State info: Info = new Info('上海');
758  @State user: User = new User();
759
760  aboutToAppear(): void {
761    this.user.info = this.info;
762  }
763
764  build() {
765    Column() {
766      Text(`${this.info.address}`);
767      Text(`${this.user.info.address}`);
768      Button('change')
769        .onClick(() => {
770          this.user.info = new Info('广州');
771          this.user.info.address = '北京';
772        })
773    }
774  }
775}
776```
777
778上述示例中,点击Button('change'),只会触发第二个Text组件的刷新。这是因为点击按钮后,首先执行`this.user.info = new Info('广州')`,会创建一个新的Info对象。再执行`this.user.info.address = '北京'`,改变的是这个新创建的Info对象中的address值,而原始的Info对象中的address值不会受到影响。
779
780### 复杂类型常量重复赋值给状态变量触发刷新
781
782```ts
783class DataObj {
784  name: string = 'default name';
785
786  constructor(name: string) {
787    this.name = name;
788  }
789}
790
791@Entry
792@Component
793struct Index {
794  list: DataObj[] = [new DataObj('a'), new DataObj('b'), new DataObj('c')];
795  @State dataObjFromList: DataObj = this.list[0];
796
797  build() {
798    Column() {
799      ConsumerChild({ dataObj: this.dataObjFromList })
800      Button('change to self').onClick(() => {
801        this.dataObjFromList = this.list[0];
802      })
803    }
804  }
805}
806
807@Component
808struct ConsumerChild {
809  @Link @Watch('onDataObjChange') dataObj: DataObj;
810
811  onDataObjChange() {
812    console.log("dataObj changed");
813  }
814
815  getContent() {
816    console.log(`this.dataObj.name change: ${this.dataObj.name}`);
817    return this.dataObj.name;
818  }
819
820  build() {
821    Column() {
822      Text(this.getContent()).fontSize(30)
823    }
824  }
825}
826```
827
828以上示例每次点击Button('change to self'),把相同的类常量赋值给一个Class类型的状态变量,会触发刷新并输出`this.dataObj.name change: a`日志。原因是在状态管理V1中,会给被\@Observed装饰的类对象以及使用状态变量装饰器如@State装饰的Class、Date、Map、Set、Array类型的对象添加一层代理用于观测一层属性或API调用产生的变化。
829当再次赋值list[0]时,dataObjFromList已经是一个Proxy类型,而list[0]是Object类型,判断是不相等的,因此会触发赋值和刷新。
830为了避免这种不必要的赋值和刷新,可以通过用\@Observed装饰类,或者使用[UIUtils.getTarget()](./arkts-new-getTarget.md)获取原始对象提前进行新旧值的判断,如果相同则不执行赋值。
831方法一:增加\@Observed
832
833```ts
834@Observed
835class DataObj {
836  name: string = 'default name';
837
838  constructor(name: string) {
839    this.name = name;
840  }
841}
842
843@Entry
844@Component
845struct Index {
846  list: DataObj[] = [new DataObj('a'), new DataObj('b'), new DataObj('c')];
847  @State dataObjFromList: DataObj = this.list[0];
848
849  build() {
850    Column() {
851      ConsumerChild({ dataObj: this.dataObjFromList })
852      Button('change to self').onClick(() => {
853        this.dataObjFromList = this.list[0];
854      })
855    }
856  }
857}
858
859@Component
860struct ConsumerChild {
861  @Link @Watch('onDataObjChange') dataObj: DataObj;
862
863  onDataObjChange() {
864    console.log("dataObj changed");
865  }
866
867  build() {
868    Column() {
869      Text(this.dataObj.name).fontSize(30)
870    }
871  }
872}
873```
874
875以上示例,给对应的类增加了\@Observed装饰器后,list[0]已经是Proxy类型了,这样再次赋值时,相同的对象,就不会触发刷新。
876
877方法二:使用[UIUtils.getTarget()](./arkts-new-getTarget.md)获取原始对象
878
879```ts
880import { UIUtils } from '@ohos.arkui.StateManagement';
881
882class DataObj {
883  name: string = 'default name';
884
885  constructor(name: string) {
886    this.name = name;
887  }
888}
889
890@Entry
891@Component
892struct Index {
893  list: DataObj[] = [new DataObj('a'), new DataObj('b'), new DataObj('c')];
894  @State dataObjFromList: DataObj = this.list[0];
895
896  build() {
897    Column() {
898      ConsumerChild({ dataObj: this.dataObjFromList })
899      Button('change to self').onClick(() => {
900        // 获取原始对象来和新值做对比
901        if (UIUtils.getTarget(this.dataObjFromList) !== this.list[0]) {
902          this.dataObjFromList = this.list[0];
903        }
904      })
905    }
906  }
907}
908
909@Component
910struct ConsumerChild {
911  @Link @Watch('onDataObjChange') dataObj: DataObj;
912
913  onDataObjChange() {
914    console.log("dataObj changed");
915  }
916
917  build() {
918    Column() {
919      Text(this.dataObj.name).fontSize(30)
920    }
921  }
922}
923```
924
925以上示例,在赋值前,使用getTarget获取了对应状态变量的原始对象,经过对比后,如果和当前对象一样,就不赋值,不触发刷新。
926
927### 不允许在build里改状态变量
928
929不允许在build里改变状态变量,状态管理框架会在运行时报出Error级别日志。
930
931下面的示例,渲染的流程是:
932
9331. 创建Index自定义组件。
934
9352. 执行Index的build方法:
936
937    1. 创建Column组件。
938
939    2. 创建Text组件。创建Text组件的过程中,触发this.count++。
940
941    3. count的改变再次触发Text组件的刷新。
942
943    4. Text最终显示为2。
944
945```ts
946@Entry
947@Component
948struct Index {
949  @State count: number = 1;
950
951  build() {
952    Column() {
953      // 应避免直接在Text组件内改变count的值
954      Text(`${this.count++}`)
955        .width(50)
956        .height(50)
957    }
958  }
959}
960```
961
962在首次创建的过程中,Text组件被多渲染了一次,导致其最终显示为2。
963
964框架识别到在build里改变状态变量会打error日志,error日志为:
965
966```ts
967FIX THIS APPLICATION ERROR: @Component 'Index'[4]: State variable 'count' has changed during render! It's illegal to change @Component state while build (initial render or re-render) is on-going. Application error!
968```
969
970在上面的例子中,这个错误行为不会造成很严重的后果,只有Text组件多渲染了一次,所以很多开发者忽略了这个日志。
971
972但这个行为是严重错误的,会随着工程的复杂度升级,隐患越来越大。见下一个例子。
973
974```ts
975@Entry
976@Component
977struct Index {
978  @State message: number = 20;
979
980  build() {
981    Column() {
982      Text(`${this.message++}`)
983
984      Text(`${this.message++}`)
985    }
986    .height('100%')
987    .width('100%')
988  }
989}
990```
991上面示例渲染过程:
992
9931. 创建第一个Text组件,触发this.message改变。
994
9952. this.message改变又触发第二个Text组件的刷新。
996
9973. 第二个Text组件的刷新又触发this.message的改变,触发第一个Text组件刷新。
998
9994. 循环重新渲染……
1000
10015. 系统长时间无响应,appfreeze。
1002
1003所以,在build里面改变状态变量的这种行为是完全错误的。当发现“FIX THIS APPLICATION ERROR: @Component ... has changed during render! It's illegal to change @Component state while build (initial render or re-render) is on-going. Application error!”日志时,即使当下没有带来严重后果,也应该警惕。应该排查应用,修改对应的错误写法,消除该错误日志。
1004
1005### 使用a.b(this.object)形式调用,不会触发UI刷新
1006
1007在build方法内,当\@State装饰的变量是Object类型、且通过a.b(this.object)形式调用时,b方法内传入的是this.object的原始对象,修改其属性,无法触发UI刷新。如下例中,通过静态方法Balloon.increaseVolume或者this.reduceVolume修改balloon的volume时,UI不会刷新。
1008
1009【反例】
1010
1011```ts
1012class Balloon {
1013  volume: number;
1014  constructor(volume: number) {
1015    this.volume = volume;
1016  }
1017
1018  static increaseVolume(balloon:Balloon) {
1019    balloon.volume += 2;
1020  }
1021}
1022
1023@Entry
1024@Component
1025struct Index {
1026  @State balloon: Balloon = new Balloon(10);
1027
1028  reduceVolume(balloon:Balloon) {
1029    balloon.volume -= 1;
1030  }
1031
1032  build() {
1033    Column({space:8}) {
1034      Text(`The volume of the balloon is ${this.balloon.volume} cubic centimeters.`)
1035        .fontSize(30)
1036      Button(`increaseVolume`)
1037        .onClick(()=>{
1038          // 通过静态方法调用,无法触发UI刷新
1039          Balloon.increaseVolume(this.balloon);
1040        })
1041      Button(`reduceVolume`)
1042        .onClick(()=>{
1043          // 使用this通过自定义组件内部方法调用,无法触发UI刷新
1044          this.reduceVolume(this.balloon);
1045        })
1046    }
1047    .width('100%')
1048    .height('100%')
1049  }
1050}
1051```
1052
1053可以通过如下先赋值、再调用新赋值的变量的方式为this.balloon加上Proxy代理,实现UI刷新。
1054
1055【正例】
1056
1057```ts
1058class Balloon {
1059  volume: number;
1060  constructor(volume: number) {
1061    this.volume = volume;
1062  }
1063
1064  static increaseVolume(balloon:Balloon) {
1065    balloon.volume += 2;
1066  }
1067}
1068
1069@Entry
1070@Component
1071struct Index {
1072  @State balloon: Balloon = new Balloon(10);
1073
1074  reduceVolume(balloon:Balloon) {
1075    balloon.volume -= 1;
1076  }
1077
1078  build() {
1079    Column({space:8}) {
1080      Text(`The volume of the balloon is ${this.balloon.volume} cubic centimeters.`)
1081        .fontSize(30)
1082      Button(`increaseVolume`)
1083        .onClick(()=>{
1084          // 通过赋值添加 Proxy 代理
1085          let balloon1 = this.balloon;
1086          Balloon.increaseVolume(balloon1);
1087        })
1088      Button(`reduceVolume`)
1089        .onClick(()=>{
1090          // 通过赋值添加 Proxy 代理
1091          let balloon2 = this.balloon;
1092          this.reduceVolume(balloon2);
1093        })
1094    }
1095    .width('100%')
1096    .height('100%')
1097  }
1098}
1099```
1100
1101### 用注册回调的方式更改状态变量需要执行解注册
1102
1103开发者可以在aboutToAppear中注册箭头函数,并以此来改变组件中的状态变量。但需要注意的是在aboutToDisappear中将之前注册的函数置空,否则会因为箭头函数捕获了自定义组件的this实例,导致自定义组件无法被释放,从而造成内存泄漏。
1104
1105```ts
1106class Model {
1107  private callback: (() => void) | undefined = () => {};
1108
1109  add(callback: () => void): void {
1110    this.callback = callback;
1111  }
1112
1113  delete(): void {
1114    this.callback = undefined;
1115  }
1116
1117  call(): void {
1118    if (this.callback) {
1119      this.callback();
1120    }
1121  }
1122}
1123
1124let model: Model = new Model();
1125
1126@Entry
1127@Component
1128struct Test {
1129  @State count: number = 10;
1130
1131  aboutToAppear(): void {
1132    model.add(() => {
1133      this.count++;
1134    })
1135  }
1136
1137  build() {
1138    Column() {
1139      Text(`count值: ${this.count}`)
1140      Button('change')
1141        .onClick(() => {
1142          model.call();
1143        })
1144    }
1145  }
1146
1147  aboutToDisappear(): void {
1148    model.delete();
1149  }
1150}
1151```
1152
1153此外,也可以使用[LocalStorage](./arkts-localstorage.md#自定义组件外改变状态变量)的方式在自定义组件外改变状态变量。