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 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