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