• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![zh-cn_image_0000001251090821](figures/zh-cn_image_0000001251090821.png)
303
304然而,上述例子是针对某个数据对象进行的整体同步,而当开发者只想针对父组件中某个数据对象的部分信息进行同步时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现,如下图所示。
305
306![zh-cn_image_0000001206450834](figures/zh-cn_image_0000001206450834.png)
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```