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