1# \@Local装饰器:组件内部状态
2
3为了实现对\@ComponentV2装饰的自定义组件中变量变化的观测,开发者可以使用\@Local装饰器装饰变量。
4
5
6在阅读本文档前,建议提前阅读:[\@ComponentV2](./arkts-new-componentV2.md)。
7
8>**说明:**
9>
10>从API version 12开始,在\@ComponentV2装饰的自定义组件中支持使用\@Local装饰器。
11>
12
13## 概述
14
15\@Local表示组件内部的状态,使得自定义组件内部的变量具有观测变化的能力:
16
17- 被\@Local装饰的变量无法从外部初始化,因此必须在组件内部进行初始化。
18
19- 当被\@Local装饰的变量变化时,会刷新使用该变量的组件。
20
21- \@Local支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
22
23- \@Local的观测能力仅限于被装饰的变量本身。当装饰简单类型时,能够观测到对变量的赋值;当装饰对象类型时,仅能观测到对对象整体的赋值;当装饰数组类型时,能观测到数组整体以及数组元素项的变化;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见[观察变化](#观察变化)。
24
25- \@Local支持null、undefined以及联合类型。
26
27## 状态管理V1版本\@State装饰器的局限性
28
29状态管理V1使用[\@State装饰器](arkts-state.md)定义类中的状态变量。但由于\@State装饰器能够从外部初始化,因此\@State无法准确表达组件内部状态不能被外面修改的语义。
30
31```ts
32class ComponentInfo {
33  name: string;
34  count: number;
35  message: string;
36  constructor(name: string, count: number, message: string) {
37    this.name = name;
38    this.count = count;
39    this.message = message;
40  }
41}
42@Component
43struct Child {
44  @State componentInfo: ComponentInfo = new ComponentInfo("Child", 1, "Hello World");
45
46  build() {
47    Column() {
48      Text(`componentInfo.message is ${this.componentInfo.message}`)
49    }
50  }
51}
52@Entry
53@Component
54struct Index {
55  build() {
56    Column() {
57      Child({componentInfo: new ComponentInfo("Unknown", 0, "Error")})
58    }
59  }
60}
61```
62
63上述代码中,可以通过在初始化Child组件时,传入新的值来覆盖Child组件想要作为内部状态变量使用的componentInfo。但Child组件并不能感知到componentInfo从外部进行了初始化,这不利于组件内部状态的管理。因此推出\@Local装饰器表示组件的内部状态。
64
65## 装饰器说明
66
67| \@Local变量装饰器 | 说明 |
68| ------------------- | ------------------------------------------------------------ |
69| 装饰器参数 | 无。 |
70| 可装饰的变量类型 | Object、class、string、number、boolean、enum等基本类型以及Array、Date、Map、Set等内嵌类型。支持null、undefined以及联合类型。 |
71| 装饰变量的初始值 | 必须本地初始化,不允许外部传入初始化。 |
72
73## 变量传递
74
75| 传递规则       | 说明                                                      |
76| -------------- | --------------------------------------------------------- |
77| 从父组件初始化 | \@Local装饰的变量仅允许本地初始化,无法从外部传入初始化。 |
78| 初始化子组件   | \@Local装饰的变量可以初始化子组件中\@Param装饰的变量。    |
79
80## 观察变化
81
82使用\@Local装饰的变量具有被观测变化的能力。当装饰的变量发生变化时,会触发该变量绑定的UI组件刷新。
83
84- 当装饰的变量类型为boolean、string、number时,可以观察到对变量赋值的变化。
85
86  ```ts
87  @Entry
88  @ComponentV2
89  struct Index {
90    @Local count: number = 0;
91    @Local message: string = "Hello";
92    @Local flag: boolean = false;
93    build() {
94      Column() {
95        Text(`${this.count}`)
96        Text(`${this.message}`)
97        Text(`${this.flag}`)
98        Button("change Local")
99          .onClick(()=>{
100            // 当@Local装饰简单类型时,能够观测到对变量的赋值
101            this.count++;
102            this.message += " World";
103            this.flag = !this.flag;
104        })
105      }
106    }
107  }
108  ```
109
110- 当装饰的变量类型为类对象时,仅可以观察到对类对象整体赋值的变化,无法直接观察到对类成员属性赋值的变化,对类成员属性的观察依赖\@ObservedV2和\@Trace装饰器。注意,\@Local无法和\@Observed装饰的类实例对象混用。
111
112    ```ts
113    class RawObject {
114      name: string;
115      constructor(name: string) {
116        this.name = name;
117      }
118    }
119    @ObservedV2
120    class ObservedObject {
121      @Trace name: string;
122      constructor(name: string) {
123        this.name = name;
124      }
125    }
126    @Entry
127    @ComponentV2
128    struct Index {
129      @Local rawObject: RawObject = new RawObject("rawObject");
130      @Local observedObject: ObservedObject = new ObservedObject("observedObject");
131      build() {
132        Column() {
133          Text(`${this.rawObject.name}`)
134          Text(`${this.observedObject.name}`)
135          Button("change object")
136            .onClick(() => {
137              // 对类对象整体的修改均能观察到
138              this.rawObject = new RawObject("new rawObject");
139              this.observedObject = new ObservedObject("new observedObject");
140          })
141          Button("change name")
142            .onClick(() => {
143              // @Local不具备观察类对象属性的能力,因此对rawObject.name的修改无法观察到
144              this.rawObject.name = "new rawObject name";
145              // 由于ObservedObject的name属性被@Trace装饰,因此对observedObject.name的修改能被观察到
146              this.observedObject.name = "new observedObject name";
147          })
148        }
149      }
150    }
151    ```
152
153- 当装饰的变量类型为简单类型的数组时,可以观察到数组整体或数组项的变化。
154
155    ```ts
156    @Entry
157    @ComponentV2
158    struct Index {
159      @Local numArr: number[] = [1,2,3,4,5];
160      @Local dimensionTwo: number[][] = [[1,2,3],[4,5,6]];
161
162      build() {
163        Column() {
164          Text(`${this.numArr[0]}`)
165          Text(`${this.numArr[1]}`)
166          Text(`${this.numArr[2]}`)
167          Text(`${this.dimensionTwo[0][0]}`)
168          Text(`${this.dimensionTwo[1][1]}`)
169          Button("change array item")
170            .onClick(() => {
171              this.numArr[0]++;
172              this.numArr[1] += 2;
173              this.dimensionTwo[0][0] = 0;
174              this.dimensionTwo[1][1] = 0;
175            })
176          Button("change whole array")
177            .onClick(() => {
178              this.numArr = [5,4,3,2,1];
179              this.dimensionTwo = [[7,8,9],[0,1,2]];
180            })
181        }
182      }
183    }
184    ```
185
186- 当装饰的变量是嵌套类或对象数组时,\@Local无法观察深层对象属性的变化。对深层对象属性的观测依赖\@ObservedV2与\@Trace装饰器。
187
188  ```ts
189  @ObservedV2
190  class Region {
191    @Trace x: number;
192    @Trace y: number;
193    constructor(x: number, y: number) {
194      this.x = x;
195      this.y = y;
196    }
197  }
198  @ObservedV2
199  class Info {
200    @Trace region: Region;
201    @Trace name: string;
202    constructor(name: string, x: number, y: number) {
203      this.name = name;
204      this.region = new Region(x, y);
205    }
206  }
207  @Entry
208  @ComponentV2
209  struct Index {
210    @Local infoArr: Info[] = [new Info("Ocean", 28, 120), new Info("Mountain", 26, 20)];
211    @Local originInfo: Info = new Info("Origin", 0, 0);
212    build() {
213      Column() {
214        ForEach(this.infoArr, (info: Info) => {
215          Row() {
216            Text(`name: ${info.name}`)
217            Text(`region: ${info.region.x}-${info.region.y}`)
218          }
219        })
220        Row() {
221            Text(`Origin name: ${this.originInfo.name}`)
222            Text(`Origin region: ${this.originInfo.region.x}-${this.originInfo.region.y}`)
223        }
224        Button("change infoArr item")
225          .onClick(() => {
226            // 由于属性name被@Trace装饰,所以能够观察到
227            this.infoArr[0].name = "Win";
228          })
229        Button("change originInfo")
230          .onClick(() => {
231            // 由于变量originInfo被@Local装饰,所以能够观察到
232            this.originInfo = new Info("Origin", 100, 100);
233          })
234        Button("change originInfo region")
235          .onClick(() => {
236            // 由于属性x、y被@Trace装饰,所以能够观察到
237            this.originInfo.region.x = 25;
238            this.originInfo.region.y = 25;
239          })
240      }
241    }
242  }
243  ```
244
245- 当装饰的变量类型是内置类型时,可以观察到变量整体赋值以及通过API调用带来的变化。
246
247  | 类型  | 可观测变化的API                                              |
248  | ----- | ------------------------------------------------------------ |
249  | Array | push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort |
250  | Date  | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds |
251  | Map   | set, clear, delete                                           |
252  | Set   | add, clear, delete                                           |
253
254## 限制条件
255
256\@Local装饰器存在以下使用限制:
257
258- \@Local装饰器只能在\@ComponentV2装饰的自定义组件中使用。
259
260  ```ts
261  @ComponentV2
262  struct MyComponent {
263    @Local message: string = "Hello World"; // 正确用法
264    build() {
265    }
266  }
267  @Component
268  struct TestComponent {
269    @Local message: string = "Hello World"; // 错误用法,编译时报错
270    build() {
271    }
272  }
273  ```
274
275- \@Local装饰的变量表示组件内部状态,不允许从外部传入初始化
276
277  ```ts
278  @ComponentV2
279  struct ChildComponent {
280    @Local message: string = "Hello World";
281    build() {
282    }
283  }
284  @ComponentV2
285  struct MyComponent {
286    build() {
287      ChildComponent({ message: "Hello" }) // 错误用法,编译时报错
288    }
289  }
290  ```
291
292## \@Local与\@State对比
293
294\@Local与\@State的用法、功能对比如下:
295
296|                    | \@State                      | \@Local                         |
297| ------------------ | ---------------------------- | --------------------------------- |
298| 参数               | 无。                          | 无。                       |
299| 从父组件初始化         | 可选。                  | 不允许外部初始化。           |
300| 观察能力 | 能观测变量本身以及一层的成员属性,无法深度观测。 | 能观测变量本身,深度观测依赖\@Trace装饰器。 |
301| 数据传递 | 可以作为数据源和子组件中状态变量同步。 | 可以作为数据源和子组件中状态变量同步。 |
302
303## 使用场景
304
305### 观测对象整体变化
306
307被\@ObservedV2与\@Trace装饰的类对象实例,具有深度观测对象属性的能力。但当对对象整体赋值时,UI却无法刷新。使用\@Local装饰对象,可以达到观测对象本身变化的效果。
308
309```ts
310@ObservedV2
311class Info {
312  @Trace name: string;
313  @Trace age: number;
314  constructor(name: string, age: number) {
315    this.name = name;
316    this.age = age;
317  }
318}
319@Entry
320@ComponentV2
321struct Index {
322  info: Info = new Info("Tom", 25);
323  @Local localInfo: Info = new Info("Tom", 25);
324  build() {
325    Column() {
326      Text(`info: ${this.info.name}-${this.info.age}`) // Text1
327      Text(`localInfo: ${this.localInfo.name}-${this.localInfo.age}`) // Text2
328      Button("change info&localInfo")
329        .onClick(() => {
330          this.info = new Info("Lucy", 18); // Text1不会刷新
331          this.localInfo = new Info("Lucy", 18); // Text2会刷新
332      })
333    }
334  }
335}
336```
337
338### 装饰Date类型变量
339
340当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。
341
342```ts
343@Entry
344@ComponentV2
345struct DatePickerExample {
346  @Local selectedDate: Date = new Date('2021-08-08');
347
348  build() {
349    Column() {
350      Button('set selectedDate to 2023-07-08')
351        .margin(10)
352        .onClick(() => {
353          this.selectedDate = new Date('2023-07-08');
354        })
355      Button('increase the year by 1')
356        .margin(10)
357        .onClick(() => {
358          this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
359        })
360      Button('increase the month by 1')
361        .margin(10)
362        .onClick(() => {
363          this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
364        })
365      Button('increase the day by 1')
366        .margin(10)
367        .onClick(() => {
368          this.selectedDate.setDate(this.selectedDate.getDate() + 1);
369        })
370      DatePicker({
371        start: new Date('1970-1-1'),
372        end: new Date('2100-1-1'),
373        selected: this.selectedDate
374      })
375    }.width('100%')
376  }
377}
378```
379
380### 装饰Map类型变量
381
382当装饰的对象是Map时,可以观察到对Map整体的赋值,同时可以通过调用Map的接口 set、clear、delete更新Map中的数据。
383
384```ts
385@Entry
386@ComponentV2
387struct MapSample {
388  @Local message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);
389
390  build() {
391    Row() {
392      Column() {
393        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
394          Text(`${item[0]}`).fontSize(30)
395          Text(`${item[1]}`).fontSize(30)
396          Divider()
397        })
398        Button('init map').onClick(() => {
399          this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]);
400        })
401        Button('set new one').onClick(() => {
402          this.message.set(4, "d");
403        })
404        Button('clear').onClick(() => {
405          this.message.clear();
406        })
407        Button('replace the first one').onClick(() => {
408          this.message.set(0, "aa");
409        })
410        Button('delete the first one').onClick(() => {
411          this.message.delete(0);
412        })
413      }
414      .width('100%')
415    }
416    .height('100%')
417  }
418}
419```
420
421### 装饰Set类型变量
422
423当装饰的对象是Set时,可以观察到对Set整体的赋值,同时可以通过调用Set的接口add、clear、delete更新Set中的数据。
424
425```ts
426@Entry
427@ComponentV2
428struct SetSample {
429  @Local message: Set<number> = new Set([0, 1, 2, 3, 4]);
430
431  build() {
432    Row() {
433      Column() {
434        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
435          Text(`${item[0]}`).fontSize(30)
436          Divider()
437        })
438        Button('init set').onClick(() => {
439          this.message = new Set([0, 1, 2, 3, 4]);
440        })
441        Button('set new one').onClick(() => {
442          this.message.add(5);
443        })
444        Button('clear').onClick(() => {
445          this.message.clear();
446        })
447        Button('delete the first one').onClick(() => {
448          this.message.delete(0);
449        })
450      }
451      .width('100%')
452    }
453    .height('100%')
454  }
455}
456```
457
458### 联合类型
459
460\@Local支持null、undefined以及联合类型。在下面的示例中,count类型为number | undefined,点击改变count的类型,UI会随之刷新。
461
462```ts
463@Entry
464@ComponentV2
465struct Index {
466  @Local count: number | undefined = 10;
467
468  build() {
469    Column() {
470      Text(`count(${this.count})`)
471      Button("change to undefined")
472        .onClick(() => {
473          this.count = undefined;
474        })
475      Button("change to number")
476        .onClick(() => {
477          this.count = 10;
478      })
479    }
480  }
481}
482```
483
484## 常见问题
485
486### 复杂类型常量重复赋值给状态变量触发刷新
487
488```ts
489@Entry
490@ComponentV2
491struct Index {
492  list: string[][] = [['a'], ['b'], ['c']];
493  @Local dataObjFromList: string[] = this.list[0];
494
495  @Monitor("dataObjFromList")
496  onStrChange(monitor: IMonitor) {
497    console.log("dataObjFromList has changed");
498  }
499
500  build() {
501    Column() {
502      Button('change to self').onClick(() => {
503        // 新值和本地初始化的值相同
504        this.dataObjFromList = this.list[0];
505      })
506    }
507  }
508}
509```
510
511以上示例每次点击Button('change to self'),把相同的Array类型常量赋值给一个Array类型的状态变量,都会触发刷新。原因是在状态管理V2中,会给使用状态变量装饰器如@Trace、@Local装饰的Date、Map、Set、Array添加一层代理用于观测API调用产生的变化。
512当再次赋值list[0]时,dataObjFromList已经是一个Proxy类型,而list[0]是Array类型,判断是不相等的,因此会触发赋值和刷新。
513为了避免这种不必要的赋值和刷新,可以使用[UIUtils.getTarget()](./arkts-new-getTarget.md)获取原始对象提前进行新旧值的判断,当两者相同时不执行赋值。
514
515使用UIUtils.getTarget()方法示例
516
517```ts
518import { UIUtils } from '@ohos.arkui.StateManagement';
519
520@Entry
521@ComponentV2
522struct Index {
523  list: string[][] = [['a'], ['b'], ['c']];
524  @Local dataObjFromList: string[] = this.list[0];
525
526  @Monitor("dataObjFromList")
527  onStrChange(monitor: IMonitor) {
528    console.log("dataObjFromList has changed");
529  }
530
531  build() {
532    Column() {
533      Button('change to self').onClick(() => {
534        // 获取原始对象来和新值做对比
535        if (UIUtils.getTarget(this.dataObjFromList) !== this.list[0]) {
536          this.dataObjFromList = this.list[0];
537        }
538      })
539    }
540  }
541}
542```