1# 页面级变量的状态管理 2 3@State、@Prop、@Link、@Provide、@Consume、@ObjectLink、@Observed和@Watch用于管理页面级变量的状态。 4 5请参考[状态变量多种数据类型声明的使用限制](./arkts-restrictions-and-extensions.md)了解@State、@Provide、 @Link和@Consume四种状态变量的约束条件。 6 7## @State 8 9@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。 10 11@State状态数据具有以下特征: 12 13- 支持多种类型数据:支持class、number、boolean、string强类型数据的值类型和引用类型,以及这些强类型构成的数组,即Array<class>、Array<string>、Array<boolean>、Array<number>。不支持object和any。 14- 支持多实例:组件不同实例的内部状态数据独立。 15- 内部私有:标记为@State的属性是私有变量,只能在组件内访问。 16- 需要本地初始化:必须为所有@State变量分配初始值,变量未初始化可能导致未定义的框架异常行为。 17- 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态变量的初始值。 18 19**示例:** 20 21在下面的示例中: 22 23- 用户定义的组件MyComponent定义了@State状态变量count和title。如果count或title的值发生变化,则执行MyComponent的build方法来重新渲染组件; 24 25- EntryComponent中有多个MyComponent组件实例,第一个MyComponent内部状态的更改不会影响第二个MyComponent; 26 27- 创建MyComponent实例时通过变量名给组件内的变量进行初始化,如: 28 29 ```ts 30 MyComponent({ title: { value: 'Hello World 2' }, count: 7 }) 31 ``` 32 33```ts 34// xxx.ets 35class Model { 36 value: string 37 38 constructor(value: string) { 39 this.value = value 40 } 41} 42 43@Entry 44@Component 45struct EntryComponent { 46 build() { 47 Column() { 48 MyComponent({ count: 1, increaseBy: 2 }) // 第1个MyComponent实例 49 MyComponent({ title: { value: 'Hello World 2' }, count: 7 }) // 第2个MyComponent实例 50 } 51 } 52} 53 54@Component 55struct MyComponent { 56 @State title: Model = { value: 'Hello World' } 57 @State count: number = 0 58 private toggle: string = 'Hello World' 59 private increaseBy: number = 1 60 61 build() { 62 Column() { 63 Text(`${this.title.value}`).fontSize(30) 64 Button('Click to change title') 65 .margin(20) 66 .onClick(() => { 67 // 修改内部状态变量title 68 this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello ArkUI' 69 }) 70 71 Button(`Click to increase count=${this.count}`) 72 .margin(20) 73 .onClick(() => { 74 // 修改内部状态变量count 75 this.count += this.increaseBy 76 }) 77 } 78 } 79} 80``` 81 82 83## @Prop 84 85@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但变量的更改不会通知给父组件,父组件变量的更改会同步到@prop装饰的变量,即@Prop属于单向数据绑定。 86 87@Prop状态数据具有以下特征: 88 89- 支持简单类型:仅支持number、string、boolean等简单数据类型; 90- 私有:仅支持组件内访问; 91- 支持多个实例:一个组件中可以定义多个标有@Prop的属性; 92- 创建自定义组件时将值传递给@Prop变量进行初始化:在创建组件的新实例时,必须初始化所有@Prop变量,不支持在组件内部进行初始化。 93 94**示例:** 95 96在下面的示例中,当按“+1”或“-1”按钮时,父组件状态发生变化,重新执行build方法,此时将创建一个新的CountDownComponent组件实例。父组件的countDownStartValue状态变量被用于初始化子组件的@Prop变量,当按下子组件的“count - costOfOneAttempt”按钮时,其@Prop变量count将被更改,CountDownComponent重新渲染,但是count值的更改不会影响父组件的countDownStartValue值。 97 98```ts 99// xxx.ets 100@Entry 101@Component 102struct ParentComponent { 103 @State countDownStartValue: number = 10 // 初始化countDownStartValue 104 105 build() { 106 Column() { 107 Text(`Grant ${this.countDownStartValue} nuggets to play.`).fontSize(18) 108 Button('+1 - Nuggets in New Game') 109 .margin(15) 110 .onClick(() => { 111 this.countDownStartValue += 1 112 }) 113 114 Button('-1 - Nuggets in New Game') 115 .margin(15) 116 .onClick(() => { 117 this.countDownStartValue -= 1 118 }) 119 // 创建子组件时,必须在构造函数参数中提供其@Prop变量count的初始值,同时初始化常规变量costOfOneAttempt(非Prop变量) 120 CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 }) 121 } 122 } 123} 124 125@Component 126struct CountDownComponent { 127 @Prop count: number 128 private costOfOneAttempt: number 129 130 build() { 131 Column() { 132 if (this.count > 0) { 133 Text(`You have ${this.count} Nuggets left`).fontSize(18) 134 } else { 135 Text('Game over!').fontSize(18) 136 } 137 138 Button('count - costOfOneAttempt') 139 .margin(15) 140 .onClick(() => { 141 this.count -= this.costOfOneAttempt 142 }) 143 } 144 } 145} 146``` 147 148 149## @Link 150 151@Link装饰的变量可以和父组件的@State变量建立双向数据绑定: 152 153- 支持多种类型:@Link支持的数据类型与@State相同,即class、number、string、boolean或这些类型的数组; 154- 私有:仅支持组件内访问; 155- 单个数据源:父组件中用于初始化子组件@Link变量的必须是父组件定义的状态变量; 156- 双向通信:子组件对@Link变量的更改将同步修改父组件中的@State变量; 157- 创建自定义组件时需要将变量的引用传递给@Link变量,在创建组件的新实例时,必须使用命名参数初始化所有@Link变量。@Link变量可以使用@State变量或@Link变量的引用进行初始化,@State变量可以通过`'$'`操作符创建引用。 158 159> **说明:** 160> 161> @Link变量不能在组件内部进行初始化。 162 163**简单类型示例:** 164 165@Link语义是从`'$'`操作符引出,即`$isPlaying`是`this.isPlaying`内部状态的双向数据绑定。当单击子组件PlayButton中的按钮时,@Link变量更改,PlayButton与父组件中的Text和Button将同时进行刷新,同样地,当点击父组件中的Button修改`this.isPlaying`时,子组件PlayButton与父组件中的Text和Button也将同时刷新。 166 167```ts 168// xxx.ets 169@Entry 170@Component 171struct Player { 172 @State isPlaying: boolean = false 173 174 build() { 175 Column() { 176 PlayButton({ buttonPlaying: $isPlaying }) 177 Text(`Player is ${this.isPlaying ? '' : 'not'} playing`).fontSize(18) 178 Button('Parent:' + this.isPlaying) 179 .margin(15) 180 .onClick(() => { 181 this.isPlaying = !this.isPlaying 182 }) 183 } 184 } 185} 186 187@Component 188struct PlayButton { 189 @Link buttonPlaying: boolean 190 191 build() { 192 Column() { 193 Button(this.buttonPlaying ? 'pause' : 'play') 194 .margin(20) 195 .onClick(() => { 196 this.buttonPlaying = !this.buttonPlaying 197 }) 198 } 199 } 200} 201``` 202 203**复杂类型示例:** 204 205```ts 206// xxx.ets 207@Entry 208@Component 209struct Parent { 210 @State arr: number[] = [1, 2, 3] 211 212 build() { 213 Column() { 214 Child({ items: $arr }) 215 Button('Parent Button: splice') 216 .margin(10) 217 .onClick(() => { 218 this.arr.splice(0, 1, 60) 219 }) 220 ForEach(this.arr, item => { 221 Text(item.toString()).fontSize(18).margin(10) 222 }, item => item.toString()) 223 } 224 } 225} 226 227 228@Component 229struct Child { 230 @Link items: number[] 231 232 build() { 233 Column() { 234 Button('Child Button1: push') 235 .margin(15) 236 .onClick(() => { 237 this.items.push(100) 238 }) 239 Button('Child Button2: replace whole item') 240 .margin(15) 241 .onClick(() => { 242 this.items = [100, 200, 300] 243 }) 244 } 245 } 246} 247``` 248 249**@Link、@State和@Prop结合使用示例:** 250 251下面示例中,ParentView包含ChildA和ChildB两个子组件,ParentView的状态变量counter分别用于初始化ChildA的@Prop变量和ChildB的@Link变量。 252 253- ChildB使用@Link建立双向数据绑定,当ChildB修改counterRef状态变量值时,该更改将同步到ParentView和ChildA共享; 254- ChildA使用@Prop建立从ParentView到自身的单向数据绑定,当ChildA修改counterVal状态变量值时,ChildA将重新渲染,但该更改不会传达给ParentView和ChildB。 255 256```ts 257// xxx.ets 258@Entry 259@Component 260struct ParentView { 261 @State counter: number = 0 262 263 build() { 264 Column() { 265 ChildA({ counterVal: this.counter }) 266 ChildB({ counterRef: $counter }) 267 } 268 } 269} 270 271@Component 272struct ChildA { 273 @Prop counterVal: number 274 275 build() { 276 Button(`ChildA: (${this.counterVal}) + 1`) 277 .margin(15) 278 .onClick(() => { 279 this.counterVal += 1 280 }) 281 } 282} 283 284@Component 285struct ChildB { 286 @Link counterRef: number 287 288 build() { 289 Button(`ChildB: (${this.counterRef}) + 1`) 290 .margin(15) 291 .onClick(() => { 292 this.counterRef += 1 293 }) 294 } 295} 296``` 297 298## @Observed和ObjectLink数据管理 299 300当开发者需要在子组件中针对父组件的一个变量(parent_a)设置双向同步时,开发者可以在父组件中使用@State装饰变量(parent_a),并在子组件中使用@Link装饰对应的变量(child_a)。这样不仅可以实现父组件与单个子组件之间的数据同步,也可以实现父组件与多个子组件之间的数据同步。如下图所示,可以看到,父子组件针对ClassA类型的变量设置了双向同步,那么当子组件1中变量对应的属性c的值变化时,会通知父组件同步变化,而当父组件中属性c的值变化时,会通知所有子组件同步变化。 301 302 303 304然而,上述例子是针对某个数据对象进行的整体同步,而当开发者只想针对父组件中某个数据对象的部分信息进行同步时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现,如下图所示。 305 306 307 308### 设置要求 309 310- @Observed用于类,@ObjectLink用于变量。 311 312- @ObjectLink装饰的变量类型必须为类(class type)。 313 - 类要被@Observed装饰器所装饰。 314 - 不支持简单类型参数,可以使用@Prop进行单向同步。 315 316- @ObjectLink装饰的变量是不可变的。 317 - 属性的改动是被允许的,当改动发生时,如果同一个对象被多个@ObjectLink变量所引用,那么所有拥有这些变量的自定义组件都会被通知进行重新渲染。 318 319- @ObjectLink装饰的变量不可设置默认值。 320 - 必须让父组件中有一个由@State、@Link、@StorageLink、@Provide或@Consume装饰的变量所参与的TS表达式进行初始化。 321 322- @ObjectLink装饰的变量是私有变量,只能在组件内访问。 323 324 325### 示例 326 327```ts 328// xxx.ets 329// 父组件ViewB中的类对象ClassA与子组件ViewA保持数据同步时,可以使用@ObjectLink和@Observed,绑定该数据对象的父组件和其他子组件同步更新 330var nextID: number = 0 331 332@Observed 333class ClassA { 334 public name: string 335 public c: number 336 public id: number 337 338 constructor(c: number, name: string = 'OK') { 339 this.name = name 340 this.c = c 341 this.id = nextID++ 342 } 343} 344 345@Component 346struct ViewA { 347 label: string = 'ViewA1' 348 @ObjectLink a: ClassA 349 350 build() { 351 Row() { 352 Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) 353 .onClick(() => { 354 this.a.c += 1 355 }) 356 }.margin({ top: 10 }) 357 } 358} 359 360@Entry 361@Component 362struct ViewB { 363 @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)] 364 365 build() { 366 Column() { 367 ForEach(this.arrA, (item) => { 368 ViewA({ label: `#${item.id}`, a: item }) 369 }, (item) => item.id.toString()) 370 ViewA({ label: `this.arrA[first]`, a: this.arrA[0] }) 371 ViewA({ label: `this.arrA[last]`, a: this.arrA[this.arrA.length - 1] }) 372 373 Button(`ViewB: reset array`) 374 .margin({ top: 10 }) 375 .onClick(() => { 376 this.arrA = [new ClassA(0), new ClassA(0)] 377 }) 378 Button(`ViewB: push`) 379 .margin({ top: 10 }) 380 .onClick(() => { 381 this.arrA.push(new ClassA(0)) 382 }) 383 Button(`ViewB: shift`) 384 .margin({ top: 10 }) 385 .onClick(() => { 386 this.arrA.shift() 387 }) 388 }.width('100%') 389 } 390} 391``` 392 393 394## @Provide和@Consume 395 396@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。 397 398> **说明:** 399> 400> 使用@Provide和@Consume时应避免循环引用导致死循环。 401 402### @Provide 403 404| 名称 | 说明 | 405| -------------- | ------------------------------------------------------------ | 406| 装饰器参数 | 是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide('alias')这种形式。 | 407| 同步机制 | @Provide的变量类似@State,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。 | 408| 初始值 | 必须设置初始值。 | 409| 页面重渲染场景 | 触发页面渲染的修改: <br/>- 基础类型(boolean,string,number)变量的改变; <br/>- @Observed class类型变量及其属性的修改; <br/>- 添加,删除,更新数组中的元素。 | 410 411### @Consume 412 413| 类型 | 说明 | 414| ------ | ---------------- | 415| 初始值 | 不可设置默认初始值。 | 416 417### 示例 418 419```ts 420// xxx.ets 421@Entry 422@Component 423struct CompA { 424 @Provide("reviewVote") reviewVotes: number = 0; 425 426 build() { 427 Column() { 428 CompB() 429 Button(`CompA: ${this.reviewVotes}`) 430 .margin(10) 431 .onClick(() => { 432 this.reviewVotes += 1; 433 }) 434 } 435 } 436} 437 438@Component 439struct CompB { 440 build() { 441 Column() { 442 CompC() 443 } 444 } 445} 446 447@Component 448struct CompC { 449 @Consume("reviewVote") reviewVotes: number 450 451 build() { 452 Column() { 453 Button(`CompC: ${this.reviewVotes}`) 454 .margin(10) 455 .onClick(() => { 456 this.reviewVotes += 1 457 }) 458 }.width('100%') 459 } 460} 461``` 462 463## @Watch 464 465@Watch用于监听状态变量的变化,语法结构为: 466 467```ts 468@State @Watch("onChanged") count : number = 0 469``` 470 471如上所示,给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。 472 473装饰器@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@StorageProp以及@StorageLink所装饰的变量均可以通过@Watch监听其变化。 474 475 476> **说明:** 477> 478> 深层次数据修改不会触发@Watch回调,例如无法监听数组中对象值的改变。 479 480```ts 481// xxx.ets 482@Entry 483@Component 484struct CompA { 485 @State @Watch('onBasketUpdated') shopBasket: Array<number> = [7, 12, 47, 3] 486 @State totalPurchase: number = 0 487 @State addPurchase: number = 0 488 489 aboutToAppear() { 490 this.updateTotal() 491 } 492 493 updateTotal(): void { 494 let sum = 0; 495 this.shopBasket.forEach((i) => { 496 sum += i 497 }) 498 // 计算新的购物篮总价值,如果超过100,则适用折扣 499 this.totalPurchase = (sum < 100) ? sum : 0.9 * sum 500 return this.totalPurchase 501 } 502 503 // shopBasket更改时触发该方法 504 onBasketUpdated(propName: string): void { 505 this.updateTotal() 506 } 507 508 build() { 509 Column() { 510 Button('add to basket ' + this.addPurchase) 511 .margin(15) 512 .onClick(() => { 513 this.addPurchase = Math.round(100 * Math.random()) 514 this.shopBasket.push(this.addPurchase) 515 }) 516 Text(`${this.totalPurchase}`) 517 .fontSize(30) 518 } 519 } 520} 521```