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