1# \@Link装饰器:父子双向同步 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiyujia926--> 5<!--Designer: @s10021109--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9 10子组件中被\@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。 11 12在阅读\@Link文档前,建议先熟悉[\@State](./arkts-state.md)的基本用法。最佳实践请参考[状态管理最佳实践](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-status-management)。 13 14> **说明:** 15> 16> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 17> 18> 从API version 11开始,该装饰器支持在原子化服务中使用。 19 20## 概述 21 22\@Link装饰的变量与其父组件中的数据源共享相同的值。 23 24 25## 装饰器使用规则说明 26 27| \@Link变量装饰器 | 说明 | 28| ------------------------------------------------------------ | ------------------------------------------------------------ | 29| 装饰器参数 | 无。 | 30| 同步类型 | 双向同步。<br/>父组件状态变量与子组件\@Link建立双向同步,当其中一方改变时,另一方也会同步更新。 | 31| 允许装饰的变量类型 |Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>API version 10开始支持[Date类型](#装饰date类型变量)。<br/>API version 11及以上支持[Map](#装饰map类型变量)、[Set](#装饰set类型变量)类型、undefined和null类型、ArkUI框架定义的联合类型[Length](../../reference/apis-arkui/arkui-ts/ts-types.md#length)、[ResourceStr](../../reference/apis-arkui/arkui-ts/ts-types.md#resourcestr)、[ResourceColor](../../reference/apis-arkui/arkui-ts/ts-types.md#resourcecolor)类型以及这些类型的联合类型,示例见[Link支持联合类型实例](#link支持联合类型实例)。<br/>支持类型的场景请参考[观察变化](#观察变化)。| 32| 不允许装饰的变量类型 | 不支持装饰Function类型。 | 33| 被装饰变量的初始值 | 禁止本地初始化。 | 34 35 36## 变量的传递/访问规则说明 37 38| 传递/访问 | 说明 | 39| ---------- | ---------------------------------------- | 40| 从父组件初始化和更新 | 必选。<br/>允许父组件中[\@State](./arkts-state.md)、\@Link、[\@Prop](./arkts-prop.md)、[\@Provide](./arkts-provide-and-consume.md)、[\@Consume](./arkts-provide-and-consume.md)、[\@ObjectLink](./arkts-observed-and-objectlink.md)、[\@StorageLink](./arkts-appstorage.md#storagelink)、[\@StorageProp](./arkts-appstorage.md#storageprop)、[\@LocalStorageLink](./arkts-localstorage.md#localstoragelink)和[\@LocalStorageProp](./arkts-localstorage.md#localstorageprop)装饰变量初始化子组件\@Link,并建立双向绑定。<br/>- 从API version 9开始,\@Link子组件从父组件初始化\@State的语法为Comp({ aLink: this.aState }),同样支持Comp({aLink: $aState})。 | 41| 用于初始化子组件 | 允许,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide。 | 42| 是否支持组件外访问 | 私有,只能在所属组件内访问。 | 43 44 **图1** 初始化规则示意图 45 46 47 48 49## 观察变化和行为表现 50 51 52### 观察变化 53 54- 当装饰的数据类型为boolean、string、number类型时,可以同步观察到数值的变化,示例请参考[简单类型和类对象类型的@Link](#简单类型和类对象类型的link)。 55 56- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即`Object.keys(observedObject)`返回的所有属性,示例请参考[简单类型和类对象类型的@Link](#简单类型和类对象类型的link)。 57 58- 当装饰的对象是Array时,可以观察到数组添加、删除、更新数组单元的变化,示例请参考[数组类型的@Link](#数组类型的link)。 59 60- 当装饰的对象是Date时,可以观察到Date的整体赋值,以及通过调用`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds`方法更新其属性,示例请参考[装饰Date类型变量](#装饰date类型变量)。 61 62- 当装饰的变量是Map时,可以观察到Map整体的赋值,以及可通过调用Map的`set`、`clear`、`delete`接口更新Map的值,示例请参考[装饰Map类型变量](#装饰map类型变量)。 63 64- 当装饰的变量是Set时,可以观察Set整体的赋值,以及通过调用Set的`add`、`clear`、`delete`接口更新其值,示例请参考[装饰Set类型变量](#装饰set类型变量)。 65### 框架行为 66 67\@Link装饰的变量和所属的自定义组件共享生命周期。 68 69为了了解\@Link变量的初始化和更新机制,有必要先了解父组件和拥有\@Link变量的子组件的关系,以及初始渲染和双向更新的流程(以父组件为\@State为例)。 70 711. 初始渲染:执行父组件的 `build()` 函数,创建子组件的新实例。初始化过程如下: 72 1. 指定父组件中的\@State变量用于初始化子组件的\@Link变量。子组件的\@Link变量值与其父组件的数据源变量保持双向数据同步。 73 2. 父组件的\@State状态变量包装类通过构造函数传给子组件,子组件的\@Link包装类拿到父组件的\@State的状态变量后,将当前\@Link包装类实例注册给父组件的\@State变量。 74 752. \@Link的数据源的更新:即父组件中状态变量更新,引起相关子组件的\@Link的更新。处理步骤: 76 1. 通过初始渲染的步骤可知,子组件\@Link包装类把当前this指针注册给父组件。父组件\@State变量变更后,会遍历更新所有依赖它的系统组件和状态变量(例如:\@Link包装类)。 77 2. 通知\@Link包装类更新后,子组件中所有依赖\@Link状态变量的系统组件都会被通知更新。以此实现父组件对子组件的状态数据同步。 78 793. \@Link的更新:当子组件中\@Link更新后,处理步骤如下(以父组件为\@State为例): 80 1. \@Link更新后,调用父组件的\@State包装类的set方法,将数值同步回父组件。 81 2. 子组件\@Link和父组件\@State分别遍历依赖的系统组件,更新对应的UI。从而实现子组件\@Link与父组件\@State的同步。 82 83 84## 限制条件 85 861. \@Link装饰器不建议在[\@Entry](./arkts-create-custom-components.md#entry)装饰的自定义组件中使用,否则编译时会抛出警告;若该自定义组件作为页面根节点使用,则会抛出运行时错误。 87 882. \@Link装饰的变量禁止本地初始化,否则编译期会报错。 89 90 ```ts 91 // 错误写法,编译报错 92 @Link count: number = 10; 93 94 // 正确写法 95 @Link count: number; 96 ``` 97 983. \@Link装饰的变量的类型要和数据源类型保持一致,否则编译期会报错。同时,数据源必须是状态变量,否则框架会抛出运行时错误。 99 100 【反例】 101 102 ```ts 103 class Info { 104 value: string = 'Hello'; 105 } 106 107 class Cousin { 108 name: string = 'Hello'; 109 } 110 111 @Component 112 struct Child { 113 // 错误写法1:@Link装饰的变量与@State装饰的变量类型不一致 114 @Link test: Cousin; 115 // 错误写法2:数据源非状态变量 116 @Link testStr: string; 117 118 build() { 119 Column() { 120 Text(this.test.name) 121 Text(this.testStr) 122 } 123 } 124 } 125 126 @Entry 127 @Component 128 struct LinkExample { 129 @State info: Info = new Info(); 130 131 build() { 132 Column() { 133 Child({ 134 // 错误写法1:@Link装饰的变量与@State装饰的变量类型不一致 135 test: this.info, 136 // 错误写法2:数据源非状态变量 137 testStr: this.info.value 138 }) 139 } 140 } 141 } 142 ``` 143 144 【正例】 145 146 ```ts 147 class Info { 148 value: string = 'Hello'; 149 } 150 151 @Component 152 struct Child { 153 // 在子组件中,使用@Link装饰Info类型的test变量 154 @Link test: Info; 155 156 build() { 157 Text(this.test.value) 158 } 159 } 160 161 @Entry 162 @Component 163 struct LinkExample { 164 @State info: Info = new Info(); 165 166 build() { 167 Column() { 168 // 在父组件中,使用@State装饰的info变量初始化Child组件的test变量 169 Child({test: this.info}) 170 } 171 } 172 } 173 ``` 174 1754. \@Link装饰的变量仅能被状态变量初始化,不能使用常规变量初始化,否则编译期会给出告警,并在运行时崩溃。 176 177 【反例】 178 179 ```ts 180 class Info { 181 info: string = 'Hello'; 182 } 183 184 @Component 185 struct Child { 186 @Link msg: string; 187 @Link info: string; 188 189 build() { 190 Text(this.msg + this.info) 191 } 192 } 193 194 @Entry 195 @Component 196 struct LinkExample { 197 @State message: string = 'Hello'; 198 @State info: Info = new Info(); 199 200 build() { 201 Column() { 202 // 错误写法,常规变量不能初始化@Link 203 Child({msg: 'World', info: this.info.info}) 204 } 205 } 206 } 207 ``` 208 209 【正例】 210 211 ```ts 212 class Info { 213 info: string = 'Hello'; 214 } 215 216 @Component 217 struct Child { 218 @Link msg: string; 219 @Link info: Info; 220 221 build() { 222 Text(this.msg + this.info.info) 223 } 224 } 225 226 @Entry 227 @Component 228 struct LinkExample { 229 @State message: string = 'Hello'; 230 @State info: Info = new Info(); 231 232 build() { 233 Column() { 234 // 正确写法 235 Child({msg: this.message, info: this.info}) 236 } 237 } 238 } 239 ``` 240 2415. \@Link不支持装饰Function类型的变量,框架会抛出运行时错误。 242 243 244## 使用场景 245 246 247### 简单类型和类对象类型的\@Link 248 249以下示例中,点击父组件ShufflingContainer中的“Parent View: Set yellowButton”和“Parent View: Set GreenButton”,可以从父组件将变化同步给子组件。 250 251 1.点击子组件GreenButton和YellowButton中的Button,子组件会发生相应变化,将变化同步给父组件。因为@Link是双向同步,会将变化同步给@State。 252 253 2.当点击父组件ShufflingContainer中的Button时,@State会发生变化,并同步给\@Link,子组件也会进行对应的刷新。 254 255```ts 256class GreenButtonState { 257 width: number = 0; 258 259 constructor(width: number) { 260 this.width = width; 261 } 262} 263 264@Component 265struct GreenButton { 266 @Link greenButtonState: GreenButtonState; 267 268 build() { 269 Button('Green Button') 270 .width(this.greenButtonState.width) 271 .height(40) 272 .backgroundColor('#64bb5c') 273 .fontColor('#FFFFFF') 274 .onClick(() => { 275 if (this.greenButtonState.width < 700) { 276 // 更新class的属性,变化可以被观察到同步回父组件 277 this.greenButtonState.width += 60; 278 } else { 279 // 更新class,变化可以被观察到同步回父组件 280 this.greenButtonState = new GreenButtonState(180); 281 } 282 }) 283 } 284} 285 286@Component 287struct YellowButton { 288 @Link yellowButtonState: number; 289 290 build() { 291 Button('Yellow Button') 292 .width(this.yellowButtonState) 293 .height(40) 294 .backgroundColor('#f7ce00') 295 .fontColor('#FFFFFF') 296 .onClick(() => { 297 // 子组件的简单类型可以同步回父组件 298 this.yellowButtonState += 40.0; 299 }) 300 } 301} 302 303@Entry 304@Component 305struct ShufflingContainer { 306 @State greenButtonState: GreenButtonState = new GreenButtonState(180); 307 @State yellowButtonProp: number = 180; 308 309 build() { 310 Column() { 311 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) { 312 // 简单类型从父组件@State向子组件@Link数据同步 313 Button('Parent View: Set yellowButton') 314 .width(this.yellowButtonProp) 315 .height(40) 316 .margin(12) 317 .fontColor('#FFFFFF') 318 .onClick(() => { 319 this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 40 : 100; 320 }) 321 // class类型从父组件@State向子组件@Link数据同步 322 Button('Parent View: Set GreenButton') 323 .width(this.greenButtonState.width) 324 .height(40) 325 .margin(12) 326 .fontColor('#FFFFFF') 327 .onClick(() => { 328 this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100; 329 }) 330 // class类型初始化@Link 331 GreenButton({ greenButtonState: this.greenButtonState }).margin(12) 332 // 简单类型初始化@Link 333 YellowButton({ yellowButtonState: this.yellowButtonProp }).margin(12) 334 } 335 } 336 } 337} 338``` 339 340 341 342### 数组类型的\@Link 343 344 345```ts 346@Component 347struct Child { 348 @Link items: number[]; 349 350 build() { 351 Column() { 352 Button(`Button1: push`) 353 .margin(12) 354 .width(312) 355 .height(40) 356 .fontColor('#FFFFFF') 357 .onClick(() => { 358 this.items.push(this.items.length + 1); 359 }) 360 Button(`Button2: replace whole item`) 361 .margin(12) 362 .width(312) 363 .height(40) 364 .fontColor('#FFFFFF') 365 .onClick(() => { 366 this.items = [100, 200, 300]; 367 }) 368 } 369 } 370} 371 372@Entry 373@Component 374struct Parent { 375 @State arr: number[] = [1, 2, 3]; 376 377 build() { 378 Column() { 379 Child({ items: $arr }) 380 .margin(12) 381 ForEach(this.arr, 382 (item: number) => { 383 Button(`${item}`) 384 .margin(12) 385 .width(312) 386 .height(40) 387 .backgroundColor('#11a2a2a2') 388 .fontColor('#e6000000') 389 }, 390 (item: ForEachInterface) => item.toString() 391 ) 392 } 393 } 394} 395``` 396 397 398 399ArkUI框架可以观察到数组元素的添加、删除和替换。在该示例中,\@State和\@Link的类型均为number[],不支持将\@Link定义成number类型(\@Link item : number),并用\@State数组中的每个数据项在父组件中创建子组件。如需使用这种场景,可以参考[\@Prop](arkts-prop.md)和[\@Observed](./arkts-observed-and-objectlink.md)。 400 401### 装饰Map类型变量 402 403> **说明:** 404> 405> 从API version 11开始,\@Link支持Map类型。 406 407在下面的示例中,value类型为Map\<number, string\>,点击Button改变message的值,视图会随之刷新。 408 409```ts 410@Component 411struct Child { 412 @Link value: Map<number, string>; 413 414 build() { 415 Column() { 416 ForEach(Array.from(this.value.entries()), (item: [number, string]) => { 417 Text(`${item[0]}`).fontSize(30) 418 Text(`${item[1]}`).fontSize(30) 419 Divider() 420 }) 421 Button('child init map').onClick(() => { 422 this.value = new Map([[0, 'a'], [1, 'b'], [3, 'c']]); 423 }) 424 Button('child set new one').onClick(() => { 425 this.value.set(4, 'd'); 426 }) 427 Button('child clear').onClick(() => { 428 this.value.clear(); 429 }) 430 Button('child replace the first one').onClick(() => { 431 this.value.set(0, 'aa'); 432 }) 433 Button('child delete the first one').onClick(() => { 434 this.value.delete(0); 435 }) 436 } 437 } 438} 439 440 441@Entry 442@Component 443struct MapSample { 444 @State message: Map<number, string> = new Map([[0, 'a'], [1, 'b'], [3, 'c']]); 445 446 build() { 447 Row() { 448 Column() { 449 Child({ value: this.message }) 450 } 451 .width('100%') 452 } 453 .height('100%') 454 } 455} 456``` 457 458### 装饰Set类型变量 459 460> **说明:** 461> 462> 从API version 11开始,\@Link支持Set类型。 463 464在下面的示例中,message类型为Set\<number\>,点击Button改变message的值,视图会随之刷新。 465 466```ts 467@Component 468struct Child { 469 @Link message: Set<number>; 470 471 build() { 472 Column() { 473 ForEach(Array.from(this.message.entries()), (item: [number, number]) => { 474 Text(`${item[0]}`).fontSize(30) 475 Divider() 476 }) 477 Button('init set').onClick(() => { 478 this.message = new Set([0, 1, 2, 3, 4]); 479 }) 480 Button('set new one').onClick(() => { 481 this.message.add(5); 482 }) 483 Button('clear').onClick(() => { 484 this.message.clear(); 485 }) 486 Button('delete the first one').onClick(() => { 487 this.message.delete(0); 488 }) 489 } 490 .width('100%') 491 } 492} 493 494 495@Entry 496@Component 497struct SetSample { 498 @State message: Set<number> = new Set([0, 1, 2, 3, 4]); 499 500 build() { 501 Row() { 502 Column() { 503 Child({ message: this.message }) 504 } 505 .width('100%') 506 } 507 .height('100%') 508 } 509} 510``` 511 512### 装饰Date类型变量 513 514在下面的示例中,selectedDate类型为Date,点击Button改变selectedDate的值,视图会随之刷新。 515 516```ts 517@Component 518struct DateComponent { 519 @Link selectedDate: Date; 520 521 build() { 522 Column() { 523 Button(`child increase the year by 1`) 524 .onClick(() => { 525 this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1); 526 }) 527 Button('child update the new date') 528 .margin(10) 529 .onClick(() => { 530 this.selectedDate = new Date('2023-09-09'); 531 }) 532 DatePicker({ 533 start: new Date('1970-1-1'), 534 end: new Date('2100-1-1'), 535 selected: this.selectedDate 536 }) 537 } 538 } 539} 540 541@Entry 542@Component 543struct ParentComponent { 544 @State parentSelectedDate: Date = new Date('2021-08-08'); 545 546 build() { 547 Column() { 548 Button('parent increase the month by 1') 549 .margin(10) 550 .onClick(() => { 551 this.parentSelectedDate.setMonth(this.parentSelectedDate.getMonth() + 1); 552 }) 553 Button('parent update the new date') 554 .margin(10) 555 .onClick(() => { 556 this.parentSelectedDate = new Date('2023-07-07'); 557 }) 558 DatePicker({ 559 start: new Date('1970-1-1'), 560 end: new Date('2100-1-1'), 561 selected: this.parentSelectedDate 562 }) 563 564 DateComponent({ selectedDate:this.parentSelectedDate }) 565 } 566 } 567} 568``` 569 570### 使用双向同步机制更改本地其他变量 571 572通过[\@Watch](./arkts-watch.md)可以在双向同步时更改本地变量。 573 574以下示例中,在\@Link的\@Watch里面修改了一个\@State装饰的变量memberMessage,实现父子组件间的变量同步。但是\@State装饰的变量memberMessage在本地修改不会影响到父组件中的变量改变。 575 576```ts 577@Entry 578@Component 579struct Parent { 580 @State sourceNumber: number = 0; 581 582 build() { 583 Column() { 584 Text(`父组件的sourceNumber:` + this.sourceNumber) 585 Child({ sourceNumber: this.sourceNumber }) 586 Button('父组件更改sourceNumber') 587 .onClick(() => { 588 this.sourceNumber++; 589 }) 590 } 591 .width('100%') 592 .height('100%') 593 } 594} 595 596@Component 597struct Child { 598 @State memberMessage: string = 'Hello World'; 599 @Link @Watch('onSourceChange') sourceNumber: number; 600 601 onSourceChange() { 602 this.memberMessage = this.sourceNumber.toString(); 603 } 604 605 build() { 606 Column() { 607 Text(this.memberMessage) 608 Text(`子组件的sourceNumber:` + this.sourceNumber.toString()) 609 Button('子组件更改memberMessage') 610 .onClick(() => { 611 this.memberMessage = 'Hello memberMessage'; 612 }) 613 } 614 } 615} 616``` 617 618### Link支持联合类型实例 619 620`@Link`支持联合类型、`undefined`和`null`。在以下示例中,`name`类型为`string | undefined`。点击父组件`Index`中的按钮可以改变`name`的属性或类型,`Child`组件也会相应刷新。 621 622```ts 623@Component 624struct Child { 625 @Link name: string | undefined; 626 627 build() { 628 Column() { 629 630 Button('Child change name to Bob') 631 .onClick(() => { 632 this.name = 'Bob'; 633 }) 634 635 Button('Child change name to undefined') 636 .onClick(() => { 637 this.name = undefined; 638 }) 639 640 }.width('100%') 641 } 642} 643 644@Entry 645@Component 646struct Index { 647 @State name: string | undefined = 'mary'; 648 649 build() { 650 Column() { 651 Text(`The name is ${this.name}`).fontSize(30) 652 653 Child({ name: this.name }) 654 655 Button('Parents change name to Peter') 656 .onClick(() => { 657 this.name = 'Peter'; 658 }) 659 660 Button('Parents change name to undefined') 661 .onClick(() => { 662 this.name = undefined; 663 }) 664 } 665 } 666} 667``` 668 669## 常见问题 670 671 672### 使用a.b(this.object)形式调用,不会触发UI刷新 673 674在build方法内,当\@Link装饰的变量是Object类型且通过a.b(this.object)形式调用时,b方法内传入的是this.object的原始对象,修改其属性无法触发UI刷新。以下示例中,通过静态方法Score.changeScore1或this.changeScore2修改Child组件中的this.score.value时,UI不会刷新。 675 676【反例】 677 678```ts 679class Score { 680 value: number; 681 constructor(value: number) { 682 this.value = value; 683 } 684 685 static changeScore1(score:Score) { 686 score.value += 1; 687 } 688} 689 690@Entry 691@Component 692struct Parent { 693 @State score: Score = new Score(1); 694 695 build() { 696 Column({space:8}) { 697 Text(`The value in Parent is ${this.score.value}.`) 698 .fontSize(30) 699 .fontColor(Color.Red) 700 Child({ score: this.score }) 701 } 702 .width('100%') 703 .height('100%') 704 } 705} 706 707@Component 708struct Child { 709 @Link score: Score; 710 711 changeScore2(score:Score) { 712 score.value += 2; 713 } 714 715 build() { 716 Column({space:8}) { 717 Text(`The value in Child is ${this.score.value}.`) 718 .fontSize(30) 719 Button(`changeScore1`) 720 .onClick(()=>{ 721 // 通过静态方法调用,无法触发UI刷新 722 Score.changeScore1(this.score); 723 }) 724 Button(`changeScore2`) 725 .onClick(()=>{ 726 // 使用this通过自定义组件内部方法调用,无法触发UI刷新 727 this.changeScore2(this.score); 728 }) 729 } 730 } 731} 732``` 733 734可以通过如下先赋值、再调用新赋值的变量的方式为this.score加上Proxy代理,实现UI刷新。 735 736【正例】 737 738```ts 739class Score { 740 value: number; 741 constructor(value: number) { 742 this.value = value; 743 } 744 745 static changeScore1(score:Score) { 746 score.value += 1; 747 } 748} 749 750@Entry 751@Component 752struct Parent { 753 @State score: Score = new Score(1); 754 755 build() { 756 Column({space:8}) { 757 Text(`The value in Parent is ${this.score.value}.`) 758 .fontSize(30) 759 .fontColor(Color.Red) 760 Child({ score: this.score }) 761 } 762 .width('100%') 763 .height('100%') 764 } 765} 766 767@Component 768struct Child { 769 @Link score: Score; 770 771 changeScore2(score:Score) { 772 score.value += 2; 773 } 774 775 build() { 776 Column({space:8}) { 777 Text(`The value in Child is ${this.score.value}.`) 778 .fontSize(30) 779 Button(`changeScore1`) 780 .onClick(()=>{ 781 // 通过赋值添加 Proxy 代理 782 let score1 = this.score; 783 Score.changeScore1(score1); 784 }) 785 Button(`changeScore2`) 786 .onClick(()=>{ 787 // 通过赋值添加 Proxy 代理 788 let score2 = this.score; 789 this.changeScore2(score2); 790 }) 791 } 792 } 793} 794``` 795 796<!--no_check--> 797