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