1# \@Param:组件外部输入 2 3 4为了增强子组件接受外部参数输入的能力,开发者可以使用\@Param装饰器。 5 6 7\@Param不仅可以接受组件外部输入,还可以接受\@Local的同步变化。在阅读本文档前,建议提前阅读:[\@Local](./arkts-new-local.md)。 8 9> **说明:** 10> 11> 从API version 12开始,在\@ComponentV2装饰的自定义组件中支持使用\@Param装饰器。 12> 13 14## 概述 15 16\@Param表示组件从外部传入的状态,使得父子组件之间的数据能够进行同步: 17 18- \@Param装饰的变量支持本地初始化,但是不允许在组件内部直接修改变量本身。 19 20- 被\@Param装饰的变量能够在初始化自定义组件时从外部传入,当数据源也是状态变量时,数据源的修改会同步给\@Param。 21- \@Param可以接受任意类型的数据源,包括普通变量、状态变量、常量、函数返回值等。 22- \@Param装饰的变量变化时,会刷新该变量关联的组件。 23- \@Param支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。 24- 对于复杂类型如类对象,\@Param会接受数据源的引用。在组件内可以修改类对象中的属性,该修改会同步到数据源。 25- \@Param的观测能力仅限于被装饰的变量本身。当装饰简单类型时,对变量的整体改变能够观测到;当装饰对象类型时,仅能观测对象整体的改变;当装饰数组类型时,能观测到数组整体以及数组元素项的改变;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见[观察变化](#观察变化)。 26- \@Param支持null、undefined以及联合类型。 27 28 29## 状态管理V1版本接受外部传入的装饰器的局限性 30状态管理V1存在多种可接受外部传入的装饰器,常用的有\@State、\@Prop、\@Link、\@ObjectLink。这些装饰器使用各有限制,不易区分,当使用不当时,还会导致性能问题。 31 32```ts 33@Observed 34class Region { 35 x: number; 36 y: number; 37 constructor(x: number, y: number) { 38 this.x = x; 39 this.y = y; 40 } 41} 42@Observed 43class Info { 44 region: Region; 45 constructor(x: number, y: number) { 46 this.region = new Region(x, y); 47 } 48} 49@Entry 50@Component 51struct Index { 52 @State info: Info = new Info(0, 0); 53 54 build() { 55 Column() { 56 Button("change Info") 57 .onClick(() => { 58 this.info = new Info(100, 100); 59 }) 60 Child({ 61 region: this.info.region, 62 regionProp: this.info.region, 63 infoProp: this.info, 64 infoLink: this.info, 65 infoState: this.info 66 }) 67 } 68 } 69} 70@Component 71struct Child { 72 @ObjectLink region: Region; 73 @Prop regionProp: Region; 74 @Prop infoProp: Info; 75 @Link infoLink: Info; 76 @State infoState: Info = new Info(1, 1); 77 build() { 78 Column() { 79 Text(`ObjectLink region: ${this.region.x}-${this.region.y}`) 80 Text(`Prop regionProp: ${this.regionProp.x}-${this.regionProp.y}`) 81 } 82 } 83} 84``` 85 86在上面的示例中,\@State仅能在初始化时获得info的引用,当改变info之后,无法进行同步。\@Prop虽然能够进行单向同步,但是对于较复杂的类型来说,深拷贝性能较差。\@Link能够接受传入的引用进行双向同步,但它必须要求数据源也是状态变量,因此无法接受info中的成员属性region。\@ObjectLink能够接受类成员属性,但是要求该属性类型必须为\@Observed装饰的类。装饰器的不同限制使得父子组件之间传值规则十分复杂,不易使用。因此推出\@Param装饰器表示组件从外部传入的状态。 87 88## 装饰器说明 89 90| \@Param变量装饰器 | 说明 | 91| ------------------ | ------------------------------------------------------------ | 92| 装饰器参数 | 无。 | 93| 能否本地修改 | 否,修改值需使用\@Event装饰器的能力。 | 94| 同步类型 | 由父到子单向同步。 | 95| 允许装饰的变量类型 | Object、class、string、number、boolean、enum等基本类型以及Array、Date、Map、Set等内嵌类型。支持null、undefined以及联合类型。 | 96| 被装饰变量的初始值 | 允许本地初始化,若不在本地初始化,则需要和\@Require装饰器一起使用,要求必须从外部传入初始化。 | 97 98## 变量传递 99 100| 传递规则 | 说明 | 101| -------------- | ------------------------------------------------------------ | 102| 从父组件初始化 | \@Param装饰的变量允许本地初始化,若无本地初始化则必须从外部传入初始化。当同时存在本地初始值与外部传入值时,会优先使用外部传入值进行初始化 | 103| 初始化子组件 | \@Param装饰的变量可以初始化子组件中\@Param装饰的变量。 | 104| 同步 | \@Param可以和父组件传入的状态变量数据源(即\@Local或\@Param装饰的变量)进行同步,当数据源发生变化时,会将修改同步给子组件的\@Param。 | 105 106## 观察变化 107 108使用\@Param装饰的变量具有被观测变化的能力。当装饰的变量发生变化时,会触发该变量绑定的UI组件刷新。 109 110- 当装饰的变量类型为boolean、string、number类型时,可以观察来自数据源同步的变化。 111 112 ```ts 113 @Entry 114 @ComponentV2 115 struct Index { 116 @Local count: number = 0; 117 @Local message: string = "Hello"; 118 @Local flag: boolean = false; 119 build() { 120 Column() { 121 Text(`Local ${this.count}`) 122 Text(`Local ${this.message}`) 123 Text(`Local ${this.flag}`) 124 Button("change Local") 125 .onClick(()=>{ 126 // 对数据源的更改会同步给子组件 127 this.count++; 128 this.message += " World"; 129 this.flag = !this.flag; 130 }) 131 Child({ 132 count: this.count, 133 message: this.message, 134 flag: this.flag 135 }) 136 } 137 } 138 } 139 @ComponentV2 140 struct Child { 141 @Require @Param count: number; 142 @Require @Param message: string; 143 @Require @Param flag: boolean; 144 build() { 145 Column() { 146 Text(`Param ${this.count}`) 147 Text(`Param ${this.message}`) 148 Text(`Param ${this.flag}`) 149 } 150 } 151 } 152 ``` 153 154- 当装饰的变量类型为类对象时,仅可以观察到对类对象整体赋值的变化,无法直接观察到对类成员属性赋值的变化,对类成员属性的观察依赖\@ObservedV2和\@Trace装饰器。 155 156 ```ts 157 class RawObject { 158 name: string; 159 constructor(name: string) { 160 this.name = name; 161 } 162 } 163 @ObservedV2 164 class ObservedObject { 165 @Trace name: string; 166 constructor(name: string) { 167 this.name = name; 168 } 169 } 170 @Entry 171 @ComponentV2 172 struct Index { 173 @Local rawObject: RawObject = new RawObject("rawObject"); 174 @Local observedObject: ObservedObject = new ObservedObject("observedObject"); 175 build() { 176 Column() { 177 Text(`${this.rawObject.name}`) 178 Text(`${this.observedObject.name}`) 179 Button("change object") 180 .onClick(() => { 181 // 对类对象整体的修改均能观察到 182 this.rawObject = new RawObject("new rawObject"); 183 this.observedObject = new ObservedObject("new observedObject"); 184 }) 185 Button("change name") 186 .onClick(() => { 187 // @Local与@Param均不具备观察类对象属性的能力,因此对rawObject.name的修改无法观察到 188 this.rawObject.name = "new rawObject name"; 189 // 由于ObservedObject的name属性被@Trace装饰,因此对observedObject.name的修改能被观察到 190 this.observedObject.name = "new observedObject name"; 191 }) 192 Child({ 193 rawObject: this.rawObject, 194 observedObject: this.observedObject 195 }) 196 } 197 } 198 } 199 @ComponentV2 200 struct Child { 201 @Require @Param rawObject: RawObject; 202 @Require @Param observedObject: ObservedObject; 203 build() { 204 Column() { 205 Text(`${this.rawObject.name}`) 206 Text(`${this.observedObject.name}`) 207 } 208 } 209 210 } 211 ``` 212 213- 当装饰的变量类型为简单类型的数组时,可以观察到数组整体或数组项的变化。 214 215 ```ts 216 @Entry 217 @ComponentV2 218 struct Index { 219 @Local numArr: number[] = [1,2,3,4,5]; 220 @Local dimensionTwo: number[][] = [[1,2,3],[4,5,6]]; 221 222 build() { 223 Column() { 224 Text(`${this.numArr[0]}`) 225 Text(`${this.numArr[1]}`) 226 Text(`${this.numArr[2]}`) 227 Text(`${this.dimensionTwo[0][0]}`) 228 Text(`${this.dimensionTwo[1][1]}`) 229 Button("change array item") 230 .onClick(() => { 231 this.numArr[0]++; 232 this.numArr[1] += 2; 233 this.dimensionTwo[0][0] = 0; 234 this.dimensionTwo[1][1] = 0; 235 }) 236 Button("change whole array") 237 .onClick(() => { 238 this.numArr = [5,4,3,2,1]; 239 this.dimensionTwo = [[7,8,9],[0,1,2]]; 240 }) 241 Child({ 242 numArr: this.numArr, 243 dimensionTwo: this.dimensionTwo 244 }) 245 } 246 } 247 } 248 @ComponentV2 249 struct Child { 250 @Require @Param numArr: number[]; 251 @Require @Param dimensionTwo: number[][]; 252 253 build() { 254 Column() { 255 Text(`${this.numArr[0]}`) 256 Text(`${this.numArr[1]}`) 257 Text(`${this.numArr[2]}`) 258 Text(`${this.dimensionTwo[0][0]}`) 259 Text(`${this.dimensionTwo[1][1]}`) 260 } 261 } 262 } 263 ``` 264 265- 当装饰的变量是嵌套类或对象数组时,\@Param无法观察深层对象属性的变化。对深层对象属性的观测依赖\@ObservedV2与\@Trace装饰器。 266 267 ```ts 268 @ObservedV2 269 class Region { 270 @Trace x: number; 271 @Trace y: number; 272 constructor(x: number, y: number) { 273 this.x = x; 274 this.y = y; 275 } 276 } 277 @ObservedV2 278 class Info { 279 @Trace region: Region; 280 @Trace name: string; 281 constructor(name: string, x: number, y: number) { 282 this.name = name; 283 this.region = new Region(x, y); 284 } 285 } 286 @Entry 287 @ComponentV2 288 struct Index { 289 @Local infoArr: Info[] = [new Info("Ocean", 28, 120), new Info("Mountain", 26, 20)]; 290 @Local originInfo: Info = new Info("Origin", 0, 0); 291 build() { 292 Column() { 293 ForEach(this.infoArr, (info: Info) => { 294 Row() { 295 Text(`name: ${info.name}`) 296 Text(`region: ${info.region.x}-${info.region.y}`) 297 } 298 }) 299 Row() { 300 Text(`Origin name: ${this.originInfo.name}`) 301 Text(`Origin region: ${this.originInfo.region.x}-${this.originInfo.region.y}`) 302 } 303 Button("change infoArr item") 304 .onClick(() => { 305 // 由于属性name被@Trace装饰,所以能够观察到 306 this.infoArr[0].name = "Win"; 307 }) 308 Button("change originInfo") 309 .onClick(() => { 310 // 由于变量originInfo被@Local装饰,所以能够观察到 311 this.originInfo = new Info("Origin", 100, 100); 312 }) 313 Button("change originInfo region") 314 .onClick(() => { 315 // 由于属性x、y被@Trace装饰,所以能够观察到 316 this.originInfo.region.x = 25; 317 this.originInfo.region.y = 25; 318 }) 319 } 320 } 321 } 322 @ComponentV2 323 struct Child { 324 @Param infoArr: Info[] = []; 325 @Param originInfo: Info = new Info("O", 0, 0); 326 327 build() { 328 Column() { 329 ForEach(this.infoArr, (info: Info) => { 330 Row() { 331 Text(`name: ${info.name}`) 332 Text(`region: ${info.region.x}-${info.region.y}`) 333 } 334 }) 335 Row() { 336 Text(`Origin name: ${this.originInfo.name}`) 337 Text(`Origin region: ${this.originInfo.region.x}-${this.originInfo.region.y}`) 338 } 339 } 340 } 341 } 342 ``` 343 344- 当装饰的变量类型是内置类型时,可以观察到变量整体赋值以及通过API调用带来的变化。 345 346 | 类型 | 可观测变化的API | 347 | ----- | ------------------------------------------------------------ | 348 | Array | push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort | 349 | Date | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds | 350 | Map | set, clear, delete | 351 | Set | add, clear, delete | 352 353## 限制条件 354 355\@Param装饰器存在以下使用限制: 356 357- \@Param装饰器只能在\@ComponentV2装饰器的自定义组件中使用。 358 359 ```ts 360 @ComponentV2 361 struct MyComponent { 362 @Param message: string = "Hello World"; // 正确用法 363 build() { 364 } 365 } 366 @Component 367 struct TestComponent { 368 @Param message: string = "Hello World"; // 错误用法,编译时报错 369 build() { 370 } 371 } 372 ``` 373 374- \@Param装饰的变量表示组件外部输入,需要被初始化。支持使用本地初始值做初始化。当存在外部传入值时,将优先使用外部传入的值初始化。既不使用本地初始值,也不使用外部传入值是不允许的。 375 376 ```ts 377 @ComponentV2 378 struct ChildComponent { 379 @Param param1: string = "Initialize local"; 380 @Param param2: string = "Initialize local and put in"; 381 @Require @Param param3: string; 382 @Param param4: string; // 错误用法,外部未传入初始化且本地也无初始值,编译报错 383 build() { 384 Column() { 385 Text(`${this.param1}`) // 本地初始化,显示Initialize local 386 Text(`${this.param2}`) // 外部传入初始化,显示Put in 387 Text(`${this.param3}`) // 外部传入初始化,显示Put in 388 } 389 } 390 } 391 @Entry 392 @ComponentV2 393 struct MyComponent { 394 @Local message: string = "Put in"; 395 build() { 396 Column() { 397 ChildComponent({ 398 param2: this.message, 399 param3: this.message 400 }) 401 } 402 } 403 } 404 ``` 405 406- \@Param装饰的变量在子组件中无法进行修改。但当装饰的变量类型为对象时,在子组件中修改对象中属性是允许的。 407 408 ```ts 409 @ObservedV2 410 class Info { 411 @Trace name: string; 412 constructor(name: string) { 413 this.name = name; 414 } 415 } 416 @Entry 417 @ComponentV2 418 struct Index { 419 @Local info: Info = new Info("Tom"); 420 build() { 421 Column() { 422 Text(`Parent info.name ${this.info.name}`) 423 Button("Parent change info") 424 .onClick(() => { 425 this.info = new Info("Lucy"); // 父组件更改@Local变量,会同步子组件对应@Param变量 426 }) 427 Child({ info: this.info }) 428 } 429 } 430 } 431 @ComponentV2 432 struct Child { 433 @Require @Param info: Info; 434 build() { 435 Column() { 436 Text(`info.name: ${this.info.name}`) 437 Button("change info") 438 .onClick(() => { 439 this.info = new Info("Jack"); //错误用法,不允许在子组件更改@Param变量,编译时报错 440 }) 441 Button("Child change info.name") 442 .onClick(() => { 443 this.info.name = "Jack"; // 允许在子组件中更改对象中属性 444 }) 445 } 446 } 447 } 448 ``` 449 450## 使用场景 451 452### 从父组件到子组件变量传递与同步 453 454\@Param能够接受父组件\@Local或\@Param传递的数据并与之变化同步。 455 456```ts 457@ObservedV2 458class Region { 459 @Trace x: number; 460 @Trace y: number; 461 constructor(x: number, y: number) { 462 this.x = x; 463 this.y = y; 464 } 465} 466@ObservedV2 467class Info { 468 @Trace name: string; 469 @Trace age: number; 470 @Trace region: Region; 471 constructor(name: string, age: number, x: number, y: number) { 472 this.name = name; 473 this.age = age; 474 this.region = new Region(x, y); 475 } 476} 477@Entry 478@ComponentV2 479struct Index { 480 @Local infoList: Info[] = [new Info("Alice", 8, 0, 0), new Info("Barry", 10, 1, 20), new Info("Cindy", 18, 24, 40)]; 481 build() { 482 Column() { 483 ForEach(this.infoList, (info: Info) => { 484 MiddleComponent({ info: info }) 485 }) 486 Button("change") 487 .onClick(() => { 488 this.infoList[0] = new Info("Atom", 40, 27, 90); 489 this.infoList[1].name = "Bob"; 490 this.infoList[2].region = new Region(7, 9); 491 }) 492 } 493 } 494} 495@ComponentV2 496struct MiddleComponent { 497 @Require @Param info: Info; 498 build() { 499 Column() { 500 Text(`name: ${this.info.name}`) 501 Text(`age: ${this.info.age}`) 502 SubComponent({ region: this.info.region }) 503 } 504 } 505} 506@ComponentV2 507struct SubComponent { 508 @Require @Param region: Region; 509 build() { 510 Column() { 511 Text(`region: ${this.region.x}-${this.region.y}`) 512 } 513 } 514} 515``` 516 517### 装饰Date类型变量 518 519\@Param装饰Date类型变量,可以观察到数据源对Date整体的赋值,以及调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 带来的变化。 520 521```ts 522@ComponentV2 523struct DateComponent { 524 @Param selectedDate: Date = new Date('2024-01-01'); 525 526 build() { 527 Column() { 528 DatePicker({ 529 start: new Date('1970-1-1'), 530 end: new Date('2100-1-1'), 531 selected: this.selectedDate 532 }) 533 } 534 } 535} 536 537@Entry 538@ComponentV2 539struct ParentComponent { 540 @Local parentSelectedDate: Date = new Date('2021-08-08'); 541 542 build() { 543 Column() { 544 Button('parent update the new date') 545 .margin(10) 546 .onClick(() => { 547 this.parentSelectedDate = new Date('2023-07-07') 548 }) 549 Button('increase the year by 1') 550 .margin(10) 551 .onClick(() => { 552 this.parentSelectedDate.setFullYear(this.parentSelectedDate.getFullYear() + 1) 553 }) 554 Button('increase the month by 1') 555 .margin(10) 556 .onClick(() => { 557 this.parentSelectedDate.setMonth(this.parentSelectedDate.getMonth() + 1) 558 }) 559 Button('parent increase the day by 1') 560 .margin(10) 561 .onClick(() => { 562 this.parentSelectedDate.setDate(this.parentSelectedDate.getDate() + 1) 563 }) 564 DateComponent({ selectedDate: this.parentSelectedDate }) 565 } 566 } 567} 568``` 569 570### 装饰Map类型变量 571 572\@Param装饰Map类型变量,可以观察到数据源对Map整体的赋值,以及调用Map的接口 set、clear、delete带来的变化。 573 574```ts 575@ComponentV2 576struct Child { 577 @Param value: Map<number, string> = new Map() 578 579 build() { 580 Column() { 581 ForEach(Array.from(this.value.entries()), (item: [number, string]) => { 582 Text(`${item[0]}`).fontSize(30) 583 Text(`${item[1]}`).fontSize(30) 584 Divider() 585 }) 586 } 587 } 588} 589@Entry 590@ComponentV2 591struct MapSample2 { 592 @Local message: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]) 593 594 build() { 595 Row() { 596 Column() { 597 Child({ value: this.message }) 598 Button('init map').onClick(() => { 599 this.message = new Map([[0, "a"], [1, "b"], [3, "c"]]) 600 }) 601 Button('set new one').onClick(() => { 602 this.message.set(4, "d") 603 }) 604 Button('clear').onClick(() => { 605 this.message.clear() 606 }) 607 Button('replace the first one').onClick(() => { 608 this.message.set(0, "aa") 609 }) 610 Button('delete the first one').onClick(() => { 611 this.message.delete(0) 612 }) 613 } 614 .width('100%') 615 } 616 .height('100%') 617 } 618} 619``` 620 621### 装饰Set类型变量 622 623\@Param装饰Set类型变量,可以观察到数据源对Set整体的赋值,以及调用Set的接口 add、clear、delete带来的变化。 624 625```ts 626@ComponentV2 627struct Child { 628 @Param message: Set<number> = new Set() 629 630 build() { 631 Column() { 632 ForEach(Array.from(this.message.entries()), (item: [number, string]) => { 633 Text(`${item[0]}`).fontSize(30) 634 Divider() 635 }) 636 } 637 .width('100%') 638 } 639} 640@Entry 641@ComponentV2 642struct SetSample11 { 643 @Local message: Set<number> = new Set([0, 1, 2, 3, 4]) 644 645 build() { 646 Row() { 647 Column() { 648 Child({ message: this.message }) 649 Button('init set').onClick(() => { 650 this.message = new Set([0, 1, 2, 3, 4]) 651 }) 652 Button('set new one').onClick(() => { 653 this.message.add(5) 654 }) 655 Button('clear').onClick(() => { 656 this.message.clear() 657 }) 658 Button('delete the first one').onClick(() => { 659 this.message.delete(0) 660 }) 661 } 662 .width('100%') 663 } 664 .height('100%') 665 } 666} 667``` 668 669### 联合类型 670 671\@Param支持null、undefined以及联合类型。在下面的示例中,count类型为number | undefined,点击改变count的类型,UI会随之刷新。 672 673```ts 674@Entry 675@ComponentV2 676struct Index { 677 @Local count: number | undefined = 0; 678 679 build() { 680 Column() { 681 MyComponent({ count: this.count }) 682 Button('change') 683 .onClick(() => { 684 this.count = undefined; 685 }) 686 } 687 } 688} 689 690@ComponentV2 691struct MyComponent { 692 @Param count: number | undefined = 0; 693 694 build() { 695 Column() { 696 Text(`count(${this.count})`) 697 } 698 } 699} 700```