1# \@State装饰器:组件内状态 2 3 4\@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。 5 6 7在状态变量相关装饰器中,\@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。 8 9 10> **说明:** 11> 12> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 13 14 15## 概述 16 17\@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。 18 19\@State装饰的变量拥有以下特点: 20 21- \@State装饰的变量与子组件中的\@Prop装饰变量之间建立单向数据同步,与\@Link、\@ObjectLink装饰变量之间建立双向数据同步。 22 23- \@State装饰的变量生命周期与其所属自定义组件的生命周期相同。 24 25 26## 装饰器使用规则说明 27 28| \@State变量装饰器 | 说明 | 29| ------------------ | ------------------------------------------------------------ | 30| 装饰器参数 | 无 | 31| 同步类型 | 不与父组件中任何类型的变量同步。 | 32| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>支持Date类型。<br/>API11及以上支持Map、Set类型。<br/>支持undefined和null类型。<br/>支持类型的场景请参考[观察变化](#观察变化)。<br/>API11及以上支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[@State支持联合类型实例](#state支持联合类型实例)。 <br/>**注意**<br/>当使用undefined和null的时候,建议显式指定类型,遵循TypeScipt类型校验,比如:`@State a : string \| undefined = undefiend`是推荐的,不推荐`@State a: string = undefined`。 33<br/>支持AkrUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 <br/>类型必须被指定。<br/>不支持any。| 34| 被装饰变量的初始值 | 必须本地初始化。 | 35 36 37## 变量的传递/访问规则说明 38 39| 传递/访问 | 说明 | 40| ------------------ | ------------------------------------------------------------ | 41| 从父组件初始化 | 可选,从父组件初始化或者本地初始化。如果从父组件初始化将会覆盖本地初始化。<br/>支持父组件中常规变量(常规变量对@State赋值,只是数值的初始化,常规变量的变化不会触发UI刷新,只有状态变量才能触发UI刷新)、\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp装饰的变量,初始化子组件的\@State。 | 42| 用于初始化子组件 | \@State装饰的变量支持初始化子组件的常规变量、\@State、\@Link、\@Prop、\@Provide。 | 43| 是否支持组件外访问 | 不支持,只能在组件内访问。 | 44 45 **图1** 初始化规则图示 46 47 48 49 50## 观察变化和行为表现 51 52并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。本小节将介绍什么样的修改才能被观察到,以及观察到变化后,框架的是怎么引起UI刷新的,即框架的行为表现是什么。 53 54 55### 观察变化 56 57- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 58 59 ```ts 60 // for simple type 61 @State count: number = 0; 62 // value changing can be observed 63 this.count = 1; 64 ``` 65 66- 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。 67 声明ClassA和Model类。 68 69 ```ts 70 class ClassA { 71 public value: string; 72 73 constructor(value: string) { 74 this.value = value; 75 } 76 } 77 78 class Model { 79 public value: string; 80 public name: ClassA; 81 constructor(value: string, a: ClassA) { 82 this.value = value; 83 this.name = a; 84 } 85 } 86 ``` 87 88 \@State装饰的类型是Model 89 90 ```ts 91 // class类型 92 @State title: Model = new Model('Hello', new ClassA('World')); 93 ``` 94 95 对\@State装饰变量的赋值。 96 97 ```ts 98 // class类型赋值 99 this.title = new Model('Hi', new ClassA('ArkUI')); 100 ``` 101 102 对\@State装饰变量的属性赋值。 103 104 ```ts 105 // class属性的赋值 106 this.title.value = 'Hi'; 107 ``` 108 109 嵌套属性的赋值观察不到。 110 111 ```ts 112 // 嵌套的属性赋值观察不到 113 this.title.name.value = 'ArkUI'; 114 ``` 115- 当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。例子如下。 116 声明Model类。 117 118 ```ts 119 class Model { 120 public value: number; 121 constructor(value: number) { 122 this.value = value; 123 } 124 } 125 ``` 126 127 \@State装饰的对象为Model类型数组时。 128 129 ```ts 130 // 数组类型 131 @State title: Model[] = [new Model(11), new Model(1)]; 132 ``` 133 134 数组自身的赋值可以观察到。 135 136 ```ts 137 // 数组赋值 138 this.title = [new Model(2)]; 139 ``` 140 141 数组项的赋值可以观察到。 142 143 ```ts 144 // 数组项赋值 145 this.title[0] = new Model(2); 146 ``` 147 148 删除数组项可以观察到。 149 150 ```ts 151 // 数组项更改 152 this.title.pop(); 153 ``` 154 155 新增数组项可以观察到。 156 157 ```ts 158 // 数组项更改 159 this.title.push(new Model(12)); 160 ``` 161 162 数组项中属性的赋值观察不到。 163 164 ```ts 165 // 嵌套的属性赋值观察不到 166 this.title[0].value = 6; 167 ``` 168 169- 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。 170 171 ```ts 172 @Entry 173 @Component 174 struct DatePickerExample { 175 @State selectedDate: Date = new Date('2021-08-08') 176 177 build() { 178 Column() { 179 Button('set selectedDate to 2023-07-08') 180 .margin(10) 181 .onClick(() => { 182 this.selectedDate = new Date('2023-07-08') 183 }) 184 Button('increase the year by 1') 185 .margin(10) 186 .onClick(() => { 187 this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1) 188 }) 189 Button('increase the month by 1') 190 .margin(10) 191 .onClick(() => { 192 this.selectedDate.setMonth(this.selectedDate.getMonth() + 1) 193 }) 194 Button('increase the day by 1') 195 .margin(10) 196 .onClick(() => { 197 this.selectedDate.setDate(this.selectedDate.getDate() + 1) 198 }) 199 DatePicker({ 200 start: new Date('1970-1-1'), 201 end: new Date('2100-1-1'), 202 selected: this.selectedDate 203 }) 204 }.width('100%') 205 } 206 } 207 ``` 208 209- 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口`set`, `clear`, `delete` 更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。 210 211- 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口`add`, `clear`, `delete` 更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。 212 213### 框架行为 214 215- 当状态变量被改变时,查询依赖该状态变量的组件; 216 217- 执行依赖该状态变量的组件的更新方法,组件更新渲染; 218 219- 和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。 220 221 222## 使用场景 223 224 225### 装饰简单类型的变量 226 227以下示例为\@State装饰的简单类型,count被\@State装饰成为状态变量,count的改变引起Button组件的刷新: 228 229- 当状态变量count改变时,查询到只有Button组件关联了它; 230 231- 执行Button组件的更新方法,实现按需刷新。 232 233 234```ts 235@Entry 236@Component 237struct MyComponent { 238 @State count: number = 0; 239 240 build() { 241 Button(`click times: ${this.count}`) 242 .onClick(() => { 243 this.count += 1; 244 }) 245 } 246} 247``` 248 249 250### 装饰class对象类型的变量 251 252- 自定义组件MyComponent定义了被\@State装饰的状态变量count和title,其中title的类型为自定义类Model。如果count或title的值发生变化,则查询MyComponent中使用该状态变量的UI组件,并进行重新渲染。 253 254- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent。 255 256 257 258```ts 259class Model { 260 public value: string; 261 262 constructor(value: string) { 263 this.value = value; 264 } 265} 266 267@Entry 268@Component 269struct EntryComponent { 270 build() { 271 Column() { 272 // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化 273 MyComponent({ count: 1, increaseBy: 2 }) 274 .width(300) 275 MyComponent({ title: new Model('Hello World 2'), count: 7 }) 276 } 277 } 278} 279 280@Component 281struct MyComponent { 282 @State title: Model = new Model('Hello World'); 283 @State count: number = 0; 284 private increaseBy: number = 1; 285 286 build() { 287 Column() { 288 Text(`${this.title.value}`) 289 .margin(10) 290 Button(`Click to change title`) 291 .onClick(() => { 292 // @State变量的更新将触发上面的Text组件内容更新 293 this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI'; 294 }) 295 .width(300) 296 .margin(10) 297 298 Button(`Click to increase count = ${this.count}`) 299 .onClick(() => { 300 // @State变量的更新将触发该Button组件的内容更新 301 this.count += this.increaseBy; 302 }) 303 .width(300) 304 .margin(10) 305 } 306 } 307} 308``` 309 310 311 312从该示例中,我们可以了解到\@State变量首次渲染的初始化流程: 313 314 3151. 使用默认的本地初始化: 316 317 ```ts 318 @State title: Model = new Model('Hello World'); 319 @State count: number = 0; 320 ``` 321 3222. 对于\@State来说,命名参数机制传递的值并不是必选的,如果没有命名参数传值,则使用本地初始化的默认值: 323 324 ```ts 325 class C1 { 326 public count:number; 327 public increaseBy:number; 328 constructor(count: number, increaseBy:number) { 329 this.count = count; 330 this.increaseBy = increaseBy; 331 } 332 } 333 let obj = new C1(1, 2) 334 MyComponent(obj) 335 ``` 336 337 338### 装饰Map类型变量 339 340> **说明:** 341> 342> 从API version 11开始,\@State支持Map类型。 343 344在下面的示例中,message类型为Map\<number, string\>,点击Button改变message的值,视图会随之刷新。 345 346```ts 347@Entry 348@Component 349struct MapSample { 350 @State message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]) 351 352 build() { 353 Row() { 354 Column() { 355 ForEach(Array.from(this.message.entries()), (item: [number, string]) => { 356 Text(`${item[0]}`).fontSize(30) 357 Text(`${item[1]}`).fontSize(30) 358 Divider() 359 }) 360 Button('init map').onClick(() => { 361 this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]) 362 }) 363 Button('set new one').onClick(() => { 364 this.message.set(4, "d") 365 }) 366 Button('clear').onClick(() => { 367 this.message.clear() 368 }) 369 Button('replace the first one').onClick(() => { 370 this.message.set(0, "aa") 371 }) 372 Button('delete the first one').onClick(() => { 373 this.message.delete(0) 374 }) 375 } 376 .width('100%') 377 } 378 .height('100%') 379 } 380} 381``` 382 383### 装饰Set类型变量 384 385> **说明:** 386> 387> 从API version 11开始,\@State支持Set类型。 388 389在下面的示例中,message类型为Set\<number\>,点击Button改变message的值,视图会随之刷新。 390 391```ts 392@Entry 393@Component 394struct SetSample { 395 @State message: Set<number> = new Set([0, 1, 2, 3, 4]) 396 397 build() { 398 Row() { 399 Column() { 400 ForEach(Array.from(this.message.entries()), (item: [number, string]) => { 401 Text(`${item[0]}`).fontSize(30) 402 Divider() 403 }) 404 Button('init set').onClick(() => { 405 this.message = new Set([0, 1, 2, 3, 4]) 406 }) 407 Button('set new one').onClick(() => { 408 this.message.add(5) 409 }) 410 Button('clear').onClick(() => { 411 this.message.clear() 412 }) 413 Button('delete the first one').onClick(() => { 414 this.message.delete(0) 415 }) 416 } 417 .width('100%') 418 } 419 .height('100%') 420 } 421} 422``` 423 424## State支持联合类型实例 425 426@State支持联合类型和undefined和null,在下面的示例中,count类型为number | undefined,点击Button改变count的属性或者类型,视图会随之刷新。 427 428```ts 429@Entry 430@Component 431struct EntryComponent { 432 build() { 433 Column() { 434 MyComponent() 435 } 436 } 437} 438 439@Component 440struct MyComponent { 441 @State count: number | undefined = 0; 442 443 build() { 444 Column() { 445 Text(`count(${this.count})`) 446 Button('change') 447 .onClick(() => { 448 this.count = undefined; 449 }) 450 } 451 } 452} 453``` 454 455 456## 常见问题 457 458### 使用箭头函数改变状态变量未生效 459 460箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。所以在该场景下, changeCoverUrl的this指向PlayDetailViewModel,而不是被装饰器@State代理的状态变量。 461 462反例: 463 464```ts 465 466export default class PlayDetailViewModel { 467 coverUrl: string = '#00ff00' 468 469 changeCoverUrl= ()=> { 470 this.coverUrl = '#00F5FF' 471 } 472 473} 474``` 475 476```ts 477import PlayDetailViewModel from './PlayDetailViewModel' 478 479@Entry 480@Component 481struct PlayDetailPage { 482 @State vm: PlayDetailViewModel = new PlayDetailViewModel() 483 484 build() { 485 Stack() { 486 Text(this.vm.coverUrl).width(100).height(100).backgroundColor(this.vm.coverUrl) 487 Row() { 488 Button('点击改变颜色') 489 .onClick(() => { 490 this.vm.changeCoverUrl() 491 }) 492 } 493 } 494 .width('100%') 495 .height('100%') 496 .alignContent(Alignment.Top) 497 } 498} 499``` 500 501所以要将当前this.vm传入,调用代理状态变量的属性赋值。 502 503正例: 504 505```ts 506 507export default class PlayDetailViewModel { 508 coverUrl: string = '#00ff00' 509 510 changeCoverUrl= (model:PlayDetailViewModel)=> { 511 model.coverUrl = '#00F5FF' 512 } 513 514} 515``` 516 517```ts 518import PlayDetailViewModel from './PlayDetailViewModel' 519 520@Entry 521@Component 522struct PlayDetailPage { 523 @State vm: PlayDetailViewModel = new PlayDetailViewModel() 524 525 build() { 526 Stack() { 527 Text(this.vm.coverUrl).width(100).height(100).backgroundColor(this.vm.coverUrl) 528 Row() { 529 Button('点击改变颜色') 530 .onClick(() => { 531 let self = this.vm 532 this.vm.changeCoverUrl(self) 533 }) 534 } 535 } 536 .width('100%') 537 .height('100%') 538 .alignContent(Alignment.Top) 539 } 540} 541``` 542 543### 状态变量的修改放在构造函数内未生效 544 545在状态管理中,类会被一层“代理”进行包装。当在组件中改变该类的成员变量时,会被该代理进行拦截,在更改数据源中值的同时,也会将变化通知给绑定的组件,从而实现观测变化与触发刷新。当开发者把状态变量的修改放在构造函数里时,此修改不会经过代理(因为是直接对数据源中的值进行修改),即使修改成功执行,也无法观测UI的刷新。 546 547【反例】 548 549```ts 550@Entry 551@Component 552struct Index { 553 @State viewModel: TestModel = new TestModel(); 554 555 build() { 556 Row() { 557 Column() { 558 Text(this.viewModel.isSuccess ? 'success' : 'failed') 559 .fontSize(50) 560 .fontWeight(FontWeight.Bold) 561 .onClick(() => { 562 this.viewModel.query() 563 }) 564 }.width('100%') 565 }.height('100%') 566 } 567} 568 569export class TestModel { 570 isSuccess: boolean = false 571 model: Model 572 573 constructor() { 574 this.model = new Model(() => { 575 this.isSuccess = true 576 console.log(`this.isSuccess: ${this.isSuccess}`) 577 }) 578 } 579 580 query() { 581 this.model.query() 582 } 583} 584 585export class Model { 586 callback: () => void 587 588 constructor(cb: () => void) { 589 this.callback = cb 590 } 591 592 query() { 593 this.callback() 594 } 595} 596``` 597 598上文示例代码将状态变量的修改放在构造函数内,界面开始时显示“failed”,点击后日志打印“this.isSuccess: true”说明修改成功,但界面依旧显示“failed”,未实现刷新。 599 600【正例】 601 602```ts 603@Entry 604@Component 605struct Index { 606 @State viewModel: TestModel = new TestModel(); 607 608 build() { 609 Row() { 610 Column() { 611 Text(this.viewModel.isSuccess ? 'success' : 'failed') 612 .fontSize(50) 613 .fontWeight(FontWeight.Bold) 614 .onClick(() => { 615 this.viewModel.query() 616 }) 617 }.width('100%') 618 }.height('100%') 619 } 620} 621 622export class TestModel { 623 isSuccess: boolean = false 624 model: Model = new Model(() => { 625 }) 626 627 query() { 628 this.model = new Model(() => { 629 this.isSuccess = true 630 }) 631 this.model.query() 632 } 633} 634 635export class Model { 636 callback: () => void 637 638 constructor(cb: () => void) { 639 this.callback = cb 640 } 641 642 query() { 643 this.callback() 644 } 645} 646``` 647 648上文示例代码将状态变量的修改放在类的普通方法中,界面开始时显示“failed”,点击后显示“success”。