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