1# \@Provide装饰器和\@Consume装饰器:与后代组件双向同步 2 3 4\@Provide和\@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,\@Provide和\@Consume摆脱参数传递机制的束缚,实现跨层级传递。 5 6 7其中\@Provide装饰的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。\@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先组件提供的变量。 8 9 10> **说明:** 11> 12> 从API version 9开始,这两个装饰器支持在ArkTS卡片中使用。 13 14 15## 概述 16 17\@Provide/\@Consume装饰的状态变量有以下特性: 18 19- \@Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,\@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。 20 21- 后代通过使用\@Consume去获取\@Provide提供的变量,建立在\@Provide和\@Consume之间的双向数据同步,与\@State/\@Link不同的是,前者可以在多层级的父子组件之间传递。 22 23- \@Provide和\@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 24 25 26```ts 27// 通过相同的变量名绑定 28@Provide a: number = 0; 29@Consume a: number; 30 31// 通过相同的变量别名绑定 32@Provide('a') b: number = 0; 33@Consume('a') c: number; 34``` 35 36 37\@Provide和\@Consume通过相同的变量名或者相同的变量别名绑定时,\@Provide装饰的变量和\@Consume装饰的变量是一对多的关系。不允许在同一个自定义组件内,包括其子组件中声明多个同名或者同别名的\@Provide装饰的变量,@Provide的属性名或别名需要唯一且确定,如果声明多个同名或者同别名的@Provide装饰的变量,会发生运行时报错。 38 39 40## 装饰器说明 41 42\@State的规则同样适用于\@Provide,差异为\@Provide还作为多层后代的同步源。 43 44| \@Provide变量装饰器 | 说明 | 45| -------------- | ---------------------------------------- | 46| 装饰器参数 | 别名:常量字符串,可选。<br/>如果指定了别名,则通过别名来绑定变量;如果未指定别名,则通过变量名绑定变量。 | 47| 同步类型 | 双向同步。<br/>从\@Provide变量到所有\@Consume变量以及相反的方向的数据同步。双向同步的操作与\@State和\@Link的组合相同。 | 48| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>支持Date类型。<br/>API11及以上支持Map、Set类型。<br/>支持类型的场景请参考[观察变化](#观察变化)。<br/>API11及以上支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[@Provide_and_Consume支持联合类型实例](#provide_and_consume支持联合类型实例)。 <br/>**注意**<br/>当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:`@Provide a : string \| undefined = undefined`是推荐的,不推荐`@Provide a: string = undefined`。 49<br/>支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。<br/>不支持any。| 必须指定类型。<br/>\@Provide变量的\@Consume变量的类型必须相同。| 50| 被装饰变量的初始值 | 必须指定。 | 51| 支持allowOverride参数 | 允许重写,只要声明了allowOverride,则别名和属性名都可以被Override。示例见\@Provide支持allowOverride参数。 | 52 53| \@Consume变量装饰器 | 说明 | 54| -------------- | ---------------------------------------- | 55| 装饰器参数 | 别名:常量字符串,可选。<br/>如果提供了别名,则必须有\@Provide的变量和其有相同的别名才可以匹配成功;否则,则需要变量名相同才能匹配成功。 | 56| 同步类型 | 双向:从\@Provide变量(具体请参见\@Provide)到所有\@Consume变量,以及相反的方向。双向同步操作与\@State和\@Link的组合相同。 | 57| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>支持Date类型。<br/>支持类型的场景请参考[观察变化](#观察变化)。<br/>API11及以上支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[@Provide_and_Consume支持联合类型实例](#provide_and_consume支持联合类型实例)。 <br/>**注意**<br/>当使用undefined和null的时候,建议显式指定类型,遵循TypeScript类型校验,比如:`@Consume a : string \| undefined`。 58<br/>支持ArkUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。<br/>不支持any。| 必须指定类型。<br/>\@Provide变量和\@Consume变量的类型必须相同。<br/>\@Consume装饰的变量,在其父组件或者祖先组件上,必须有对应的属性和别名的\@Provide装饰的变量。 | 59| 被装饰变量的初始值 | 无,禁止本地初始化。 | 60 61 62## 变量的传递/访问规则说明 63 64 65| \@Provide传递/访问 | 说明 | 66| -------------- | ---------------------------------------- | 67| 从父组件初始化和更新 | 可选,允许父组件中常规变量(常规变量对@Prop赋值,只是数值的初始化,常规变量的变化不会触发UI刷新,只有状态变量才能触发UI刷新)、\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp装饰的变量装饰变量初始化子组件\@Provide。 | 68| 用于初始化子组件 | 允许,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | 69| 和父组件同步 | 否。 | 70| 和后代组件同步 | 和\@Consume双向同步。 | 71| 是否支持组件外访问 | 私有,仅可以在所属组件内访问。 | 72 73 74 **图1** \@Provide初始化规则图示 75 76 77 78 79 80| \@Consume传递/访问 | 说明 | 81| -------------- | ---------------------------------------- | 82| 从父组件初始化和更新 | 禁止。通过相同的变量名和alias(别名)从\@Provide初始化。 | 83| 用于初始化子组件 | 允许,可用于初始化\@State、\@Link、\@Prop、\@Provide。 | 84| 和祖先组件同步 | 和\@Provide双向同步。 | 85| 是否支持组件外访问 | 私有,仅可以在所属组件内访问 | 86 87 88 **图2** \@Consume初始化规则图示 89 90 91 92 93 94## 观察变化和行为表现 95 96 97### 观察变化 98 99- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。 100 101- 当装饰的数据类型为class或者Object的时候,可以观察到赋值和属性赋值的变化(属性为Object.keys(observedObject)返回的所有属性)。 102 103- 当装饰的对象是array的时候,可以观察到数组的添加、删除、更新数组单元。 104 105- 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。 106 107```ts 108@Component 109struct CompD { 110 @Consume selectedDate: Date; 111 112 build() { 113 Column() { 114 Button(`child increase the day by 1`) 115 .onClick(() => { 116 this.selectedDate.setDate(this.selectedDate.getDate() + 1) 117 }) 118 Button('child update the new date') 119 .margin(10) 120 .onClick(() => { 121 this.selectedDate = new Date('2023-09-09') 122 }) 123 DatePicker({ 124 start: new Date('1970-1-1'), 125 end: new Date('2100-1-1'), 126 selected: this.selectedDate 127 }) 128 } 129 } 130} 131 132@Entry 133@Component 134struct CompA { 135 @Provide selectedDate: Date = new Date('2021-08-08') 136 137 build() { 138 Column() { 139 Button('parent increase the day by 1') 140 .margin(10) 141 .onClick(() => { 142 this.selectedDate.setDate(this.selectedDate.getDate() + 1) 143 }) 144 Button('parent update the new date') 145 .margin(10) 146 .onClick(() => { 147 this.selectedDate = new Date('2023-07-07') 148 }) 149 DatePicker({ 150 start: new Date('1970-1-1'), 151 end: new Date('2100-1-1'), 152 selected: this.selectedDate 153 }) 154 CompD() 155 } 156 } 157} 158``` 159 160- 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口`set`, `clear`, `delete` 更新Map的值。详见[装饰Map类型变量](#装饰map类型变量)。 161 162- 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口`add`, `clear`, `delete` 更新Set的值。详见[装饰Set类型变量](#装饰set类型变量)。 163 164### 框架行为 165 1661. 初始渲染: 167 1. \@Provide装饰的变量会以map的形式,传递给当前\@Provide所属组件的所有子组件; 168 2. 子组件中如果使用\@Consume变量,则会在map中查找是否有该变量名/alias(别名)对应的\@Provide的变量,如果查找不到,框架会抛出JS ERROR; 169 3. 在初始化\@Consume变量时,和\@State/\@Link的流程类似,\@Consume变量会保存在map中查找到的\@Provide变量,并把自己注册给\@Provide。 170 1712. 当\@Provide装饰的数据变化时: 172 1. 通过初始渲染的步骤可知,子组件\@Consume已把自己注册给父组件。父组件\@Provide变量变更后,会遍历更新所有依赖它的系统组件(elementid)和状态变量(\@Consume); 173 2. 通知\@Consume更新后,子组件所有依赖\@Consume的系统组件(elementId)都会被通知更新。以此实现\@Provide对\@Consume状态数据同步。 174 1753. 当\@Consume装饰的数据变化时: 176 177 通过初始渲染的步骤可知,子组件\@Consume持有\@Provide的实例。在\@Consume更新后调用\@Provide的更新方法,将更新的数值同步回\@Provide,以此实现\@Consume向\@Provide的同步更新。 178 179 180## 使用场景 181 182在下面的示例是与后代组件双向同步状态\@Provide和\@Consume场景。当分别点击CompA和CompD组件内Button时,reviewVotes 的更改会双向同步在CompA和CompD中。 183 184 185 186```ts 187@Component 188struct CompD { 189 // @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量 190 @Consume reviewVotes: number; 191 192 build() { 193 Column() { 194 Text(`reviewVotes(${this.reviewVotes})`) 195 Button(`reviewVotes(${this.reviewVotes}), give +1`) 196 .onClick(() => this.reviewVotes += 1) 197 } 198 .width('50%') 199 } 200} 201 202@Component 203struct CompC { 204 build() { 205 Row({ space: 5 }) { 206 CompD() 207 CompD() 208 } 209 } 210} 211 212@Component 213struct CompB { 214 build() { 215 CompC() 216 } 217} 218 219@Entry 220@Component 221struct CompA { 222 // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件 223 @Provide reviewVotes: number = 0; 224 225 build() { 226 Column() { 227 Button(`reviewVotes(${this.reviewVotes}), give +1`) 228 .onClick(() => this.reviewVotes += 1) 229 CompB() 230 } 231 } 232} 233``` 234 235### 装饰Map类型变量 236 237> **说明:** 238> 239> 从API version 11开始,\@Provide,\@Consume支持Map类型。 240 241在下面的示例中,message类型为Map<number, string>,点击Button改变message的值,视图会随之刷新。 242 243```ts 244@Component 245struct Child { 246 @Consume message: Map<number, string> 247 248 build() { 249 Column() { 250 ForEach(Array.from(this.message.entries()), (item: [number, string]) => { 251 Text(`${item[0]}`).fontSize(30) 252 Text(`${item[1]}`).fontSize(30) 253 Divider() 254 }) 255 Button('Consume init map').onClick(() => { 256 this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]) 257 }) 258 Button('Consume set new one').onClick(() => { 259 this.message.set(4, "d") 260 }) 261 Button('Consume clear').onClick(() => { 262 this.message.clear() 263 }) 264 Button('Consume replace the first item').onClick(() => { 265 this.message.set(0, "aa") 266 }) 267 Button('Consume delete the first item').onClick(() => { 268 this.message.delete(0) 269 }) 270 } 271 } 272} 273 274 275@Entry 276@Component 277struct MapSample { 278 @Provide message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]) 279 280 build() { 281 Row() { 282 Column() { 283 Button('Provide init map').onClick(() => { 284 this.message = new Map([[0, "a"], [1, "b"], [3, "c"], [4, "d"]]) 285 }) 286 Child() 287 } 288 .width('100%') 289 } 290 .height('100%') 291 } 292} 293``` 294 295### 装饰Set类型变量 296 297> **说明:** 298> 299> 从API version 11开始,\@Provide,\@Consume支持Set类型。 300 301在下面的示例中,message类型为Set\<number\>,点击Button改变message的值,视图会随之刷新。 302 303```ts 304@Component 305struct Child { 306 @Consume message: Set<number> 307 308 build() { 309 Column() { 310 ForEach(Array.from(this.message.entries()), (item: [number, string]) => { 311 Text(`${item[0]}`).fontSize(30) 312 Divider() 313 }) 314 Button('Consume init set').onClick(() => { 315 this.message = new Set([0, 1, 2, 3, 4]) 316 }) 317 Button('Consume set new one').onClick(() => { 318 this.message.add(5) 319 }) 320 Button('Consume clear').onClick(() => { 321 this.message.clear() 322 }) 323 Button('Consume delete the first one').onClick(() => { 324 this.message.delete(0) 325 }) 326 } 327 .width('100%') 328 } 329} 330 331 332@Entry 333@Component 334struct SetSample { 335 @Provide message: Set<number> = new Set([0, 1, 2, 3, 4]) 336 337 build() { 338 Row() { 339 Column() { 340 Button('Provide init set').onClick(() => { 341 this.message = new Set([0, 1, 2, 3, 4, 5]) 342 }) 343 Child() 344 } 345 .width('100%') 346 } 347 .height('100%') 348 } 349} 350``` 351 352### Provide_and_Consume支持联合类型实例 353 354@Provide和@Consume支持联合类型和undefined和null,在下面的示例中,count类型为string | undefined,点击父组件Parent中的Button改变count的属性或者类型,Child中也会对应刷新。 355 356```ts 357@Component 358struct Child { 359 // @Consume装饰的变量通过相同的属性名绑定其祖先组件Ancestors内的@Provide装饰的变量 360 @Consume count: string | undefined; 361 362 build() { 363 Column() { 364 Text(`count(${this.count})`) 365 Button(`count(${this.count}), Child`) 366 .onClick(() => this.count = 'Ancestors') 367 } 368 .width('50%') 369 } 370} 371 372@Component 373struct Parent { 374 build() { 375 Row({ space: 5 }) { 376 Child() 377 } 378 } 379} 380 381@Entry 382@Component 383struct Ancestors { 384 // @Provide装饰的联合类型count由入口组件Ancestors提供其后代组件 385 @Provide count: string | undefined = 'Child'; 386 387 build() { 388 Column() { 389 Button(`count(${this.count}), Child`) 390 .onClick(() => this.count = undefined) 391 Parent() 392 } 393 } 394} 395``` 396 397### \@Provide支持allowOverride参数 398 399allowOverride:\@Provide重写选项。 400 401> **说明:** 402> 403> 从API version 11开始使用。 404 405| 名称 | 类型 | 必填 | 说明 | 406| ------ | ------ | ---- | ------------------------------------------------------------ | 407| allowOverride | string | 否 | 是否允许@Provide重写。允许在同一组件树下通过allowOverride重写同名的@Provide。如果开发者未写allowOverride,定义同名的@Provide,运行时会报错。 | 408 409```ts 410@Component 411struct MyComponent { 412 @Provide({allowOverride : "reviewVotes"}) reviewVotes: number = 10; 413} 414``` 415 416```ts 417@Component 418struct GrandSon { 419 // @Consume装饰的变量通过相同的属性名绑定其祖先内的@Provide装饰的变量 420 @Consume("reviewVotes") reviewVotes: number; 421 422 build() { 423 Column() { 424 Text(`reviewVotes(${this.reviewVotes})`) // Text显示10 425 Button(`reviewVotes(${this.reviewVotes}), give +1`) 426 .onClick(() => this.reviewVotes += 1) 427 } 428 .width('50%') 429 } 430} 431 432@Component 433struct Child { 434 @Provide({ allowOverride: "reviewVotes" }) reviewVotes: number = 10; 435 436 build() { 437 Row({ space: 5 }) { 438 GrandSon() 439 } 440 } 441} 442 443@Component 444struct Parent { 445 @Provide({ allowOverride: "reviewVotes" }) reviewVotes: number = 20; 446 447 build() { 448 Child() 449 } 450} 451 452@Entry 453@Component 454struct GrandParent { 455 @Provide("reviewVotes") reviewVotes: number = 40; 456 457 build() { 458 Column() { 459 Button(`reviewVotes(${this.reviewVotes}), give +1`) 460 .onClick(() => this.reviewVotes += 1) 461 Parent() 462 } 463 } 464} 465``` 466 467在上面的示例中: 468- GrandParent声明了@Provide("reviewVotes") reviewVotes: number = 40 469- Parent是GrandParent的子组件,声明@Provide为allowOverride,重写父组件GrandParent的@Provide("reviewVotes") reviewVotes: number = 40。如果不设置allowOverride,则会抛出运行时报错,提示@Provide重复定义。Child同理。 470- GrandSon在初始化@Consume的时候,@Consume装饰的变量通过相同的属性名绑定其最近的祖先的@Provide装饰的变量。 471- GrandSon查找到相同属性名的@Provide在祖先Child中,所以@Consume("reviewVotes") reviewVotes: number初始化数值为10。如果Child中没有定义与@Consume同名的@Provide,则继续向上寻找Parent中的同名@Provide值为20,以此类推。 472- 如果查找到根节点还没有找到key对应的@Provide,则会报初始化@Consume找不到@Provide的报错。 473 474 475## 常见问题 476 477### \@BuilderParam尾随闭包情况下\@Provide未定义错误 478 479在此场景下,CustomWidget执行this.builder()创建子组件CustomWidgetChild时,this指向的是HomePage。因此找不到CustomWidget的\@Provide变量,所以下面示例会报找不到\@Provide错误,和\@BuilderParam连用的时候要谨慎this的指向。 480 481错误示例: 482 483```ts 484class Tmp { 485 a: string = '' 486} 487 488@Entry 489@Component 490struct HomePage { 491 @Builder 492 builder2($$: Tmp) { 493 Text(`${$$.a}测试`) 494 } 495 496 build() { 497 Column() { 498 CustomWidget() { 499 CustomWidgetChild({ builder: this.builder2 }) 500 } 501 } 502 } 503} 504 505@Component 506struct CustomWidget { 507 @Provide('a') a: string = 'abc'; 508 @BuilderParam 509 builder: () => void; 510 511 build() { 512 Column() { 513 Button('你好').onClick((x) => { 514 if (this.a == 'ddd') { 515 this.a = 'abc'; 516 } 517 else { 518 this.a = 'ddd'; 519 } 520 521 }) 522 this.builder() 523 } 524 } 525} 526 527@Component 528struct CustomWidgetChild { 529 @Consume('a') a: string; 530 @BuilderParam 531 builder: ($$: Tmp) => void; 532 533 build() { 534 Column() { 535 this.builder({ a: this.a }) 536 } 537 } 538} 539``` 540