1# \@Prop装饰器:父子单向同步 2 3 4\@Prop装饰的变量可以和父组件建立单向的同步关系。\@Prop装饰的变量是可变的,但是变化不会同步回其父组件。 5 6 7> **说明:** 8> 9> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 10 11 12## 概述 13 14\@Prop装饰的变量和父组件建立单向的同步关系: 15 16- \@Prop变量允许在本地修改,但修改后的变化不会同步回父组件。 17 18- 当数据源更改时,\@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。 19 20 21## 装饰器使用规则说明 22 23| \@Prop变量装饰器 | 说明 | 24| ----------- | ---------------------------------------- | 25| 装饰器参数 | 无 | 26| 同步类型 | 单向同步:对父组件状态变量值的修改,将同步给子组件\@Prop装饰的变量,子组件\@Prop变量的修改不会同步到父组件的状态变量上。嵌套类型的场景请参考[观察变化](#观察变化)。 | 27| 允许装饰的变量类型 | Object、class、string、number、boolean、enum类型,以及这些类型的数组。<br/>不支持any,支持undefined和null。<br/>支持Date类型。<br/>支持类型的场景请参考[观察变化](#观察变化)。<br/>支持上述支持类型的联合类型,比如string \| number, string \| undefined 或者 ClassA \| null,示例见[@Prop支持联合类型实例](#@Prop支持联合类型实例)。 <br/>**注意**<br/>当使用undefined和null的时候,建议显式指定类型,遵循TypeScipt类型校验,比如:`@Prop a : string \| undefined = undefiend`是推荐的,不推荐`@Prop a: string = undefined`。 28<br/>支持AkrUI框架定义的联合类型Length、ResourceStr、ResourceColor类型。 <br/>必须指定类型。<br/>**说明** :<br/>\@Prop和[数据源](arkts-state-management-overview.md#基本概念)类型需要相同,有以下三种情况:<br/>- \@Prop装饰的变量和\@State以及其他装饰器同步时双方的类型必须相同,示例请参考[父组件@State到子组件@Prop简单数据类型同步](#父组件state到子组件prop简单数据类型同步)。<br/>- \@Prop装饰的变量和\@State以及其他装饰器装饰的数组的项同步时 ,\@Prop的类型需要和\@State装饰的数组的数组项相同,比如\@Prop : T和\@State : Array<T>,示例请参考[父组件@State数组中的项到子组件@Prop简单数据类型同步](#父组件state数组项到子组件prop简单数据类型同步);<br/>- 当父组件状态变量为Object或者class时,\@Prop装饰的变量和父组件状态变量的属性类型相同,示例请参考[从父组件中的@State类对象属性到@Prop简单类型的同步](#从父组件中的state类对象属性到prop简单类型的同步)。 | 29| 嵌套传递层数 | 在组件复用场景,建议@Prop深度嵌套数据不要超过5层,嵌套太多会导致深拷贝占用的空间过大以及GarbageCollection(垃圾回收),引起性能问题,此时更建议使用[\@ObjectLink](arkts-observed-and-objectlink.md)。如果子组件的数据不想同步回父组件,建议采用@Reusable中的aboutToReuse,实现父组件向子组件传递数据,具体用例请参考[组件复用场景](arkts-state-management-best-practices.md)。 | 30| 被装饰变量的初始值 | 允许本地初始化。 | 31 32 33## 变量的传递/访问规则说明 34 35| 传递/访问 | 说明 | 36| --------- | ---------------------------------------- | 37| 从父组件初始化 | 如果本地有初始化,则是可选的。没有的话,则必选,支持父组件中的常规变量(常规变量对@Prop赋值,只是数值的初始化,常规变量的变化不会触发UI刷新。只有状态变量才能触发UI刷新)、\@State、\@Link、\@Prop、\@Provide、\@Consume、\@ObjectLink、\@StorageLink、\@StorageProp、\@LocalStorageLink和\@LocalStorageProp去初始化子组件中的\@Prop变量。 | 38| 用于初始化子组件 | \@Prop支持去初始化子组件中的常规变量、\@State、\@Link、\@Prop、\@Provide。 | 39| 是否支持组件外访问 | \@Prop装饰的变量是私有的,只能在组件内访问。 | 40 41 42 **图1** 初始化规则图示 43 44 45 46 47 48## 观察变化和行为表现 49 50 51### 观察变化 52 53\@Prop装饰的数据可以观察到以下变化。 54 55- 当装饰的类型是允许的类型,即Object、class、string、number、boolean、enum类型都可以观察到赋值的变化。 56 57 ```ts 58 // 简单类型 59 @Prop count: number; 60 // 赋值的变化可以被观察到 61 this.count = 1; 62 // 复杂类型 63 @Prop count: Model; 64 // 可以观察到赋值的变化 65 this.title = new Model('Hi'); 66 ``` 67 68当装饰的类型是Object或者class复杂类型时,可以观察到第一层的属性的变化,属性即Object.keys(observedObject)返回的所有属性; 69 70``` 71class ClassA { 72 public value: string; 73 constructor(value: string) { 74 this.value = value; 75 } 76} 77class Model { 78 public value: string; 79 public a: ClassA; 80 constructor(value: string, a: ClassA) { 81 this.value = value; 82 this.a = a; 83 } 84} 85 86@Prop title: Model; 87// 可以观察到第一层的变化 88this.title.value = 'Hi' 89// 观察不到第二层的变化 90this.title.a.value = 'ArkUi' 91``` 92 93对于嵌套场景,如果class是被\@Observed装饰的,可以观察到class属性的变化,示例请参考[@Prop嵌套场景](#@Prop嵌套场景)。 94 95当装饰的类型是数组的时候,可以观察到数组本身的赋值、添加、删除和更新。 96 97``` 98// @State装饰的对象为数组时 99@Prop title: string[] 100// 数组自身的赋值可以观察到 101this.title = ['1'] 102// 数组项的赋值可以观察到 103this.title[0] = '2' 104// 删除数组项可以观察到 105this.title.pop() 106// 新增数组项可以观察到 107this.title.push('3') 108``` 109 110对于\@State和\@Prop的同步场景: 111 112- 使用父组件中\@State变量的值初始化子组件中的\@Prop变量。当\@State变量变化时,该变量值也会同步更新至\@Prop变量。 113- \@Prop装饰的变量的修改不会影响其数据源\@State装饰变量的值。 114- 除了\@State,数据源也可以用\@Link或\@Prop装饰,对\@Prop的同步机制是相同的。 115- 数据源和\@Prop变量的类型需要相同,\@Prop允许简单类型和class类型。 116 117- 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。 118 119```ts 120@Component 121struct DateComponent { 122 @Prop selectedDate: Date = new Date(''); 123 124 build() { 125 Column() { 126 Button('child update the new date') 127 .margin(10) 128 .onClick(() => { 129 this.selectedDate = new Date('2023-09-09') 130 }) 131 Button(`child increase the year by 1`).onClick(() => { 132 this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1) 133 }) 134 DatePicker({ 135 start: new Date('1970-1-1'), 136 end: new Date('2100-1-1'), 137 selected: this.selectedDate 138 }) 139 } 140 } 141} 142 143@Entry 144@Component 145struct ParentComponent { 146 @State parentSelectedDate: Date = new Date('2021-08-08'); 147 148 build() { 149 Column() { 150 Button('parent update the new date') 151 .margin(10) 152 .onClick(() => { 153 this.parentSelectedDate = new Date('2023-07-07') 154 }) 155 Button('parent increase the day by 1') 156 .margin(10) 157 .onClick(() => { 158 this.parentSelectedDate.setDate(this.parentSelectedDate.getDate() + 1) 159 }) 160 DatePicker({ 161 start: new Date('1970-1-1'), 162 end: new Date('2100-1-1'), 163 selected: this.parentSelectedDate 164 }) 165 166 DateComponent({selectedDate:this.parentSelectedDate}) 167 } 168 169 } 170} 171``` 172 173### 框架行为 174 175要理解\@Prop变量值初始化和更新机制,有必要了解父组件和拥有\@Prop变量的子组件初始渲染和更新流程。 176 1771. 初始渲染: 178 1. 执行父组件的build()函数将创建子组件的新实例,将数据源传递给子组件; 179 2. 初始化子组件\@Prop装饰的变量。 180 1812. 更新: 182 1. 子组件\@Prop更新时,更新仅停留在当前子组件,不会同步回父组件; 183 2. 当父组件的数据源更新时,子组件的\@Prop装饰的变量将被来自父组件的数据源重置,所有\@Prop装饰的本地的修改将被父组件的更新覆盖。 184 185 186## 使用场景 187 188 189### 父组件\@State到子组件\@Prop简单数据类型同步 190 191 192以下示例是\@State到子组件\@Prop简单数据同步,父组件ParentComponent的状态变量countDownStartValue初始化子组件CountDownComponent中\@Prop装饰的count,点击“Try again”,count的修改仅保留在CountDownComponent 不会同步给父组件ParentComponent。 193 194 195ParentComponent的状态变量countDownStartValue的变化将重置CountDownComponent的count。 196 197 198 199```ts 200@Component 201struct CountDownComponent { 202 @Prop count: number = 0; 203 costOfOneAttempt: number = 1; 204 205 build() { 206 Column() { 207 if (this.count > 0) { 208 Text(`You have ${this.count} Nuggets left`) 209 } else { 210 Text('Game over!') 211 } 212 // @Prop装饰的变量不会同步给父组件 213 Button(`Try again`).onClick(() => { 214 this.count -= this.costOfOneAttempt; 215 }) 216 } 217 } 218} 219 220@Entry 221@Component 222struct ParentComponent { 223 @State countDownStartValue: number = 10; 224 225 build() { 226 Column() { 227 Text(`Grant ${this.countDownStartValue} nuggets to play.`) 228 // 父组件的数据源的修改会同步给子组件 229 Button(`+1 - Nuggets in New Game`).onClick(() => { 230 this.countDownStartValue += 1; 231 }) 232 // 父组件的修改会同步给子组件 233 Button(`-1 - Nuggets in New Game`).onClick(() => { 234 this.countDownStartValue -= 1; 235 }) 236 237 CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 }) 238 } 239 } 240} 241``` 242 243 244在上面的示例中: 245 246 2471. CountDownComponent子组件首次创建时其\@Prop装饰的count变量将从父组件\@State装饰的countDownStartValue变量初始化; 248 2492. 按“+1”或“-1”按钮时,父组件的\@State装饰的countDownStartValue值会变化,这将触发父组件重新渲染,在父组件重新渲染过程中会刷新使用countDownStartValue状态变量的UI组件并单向同步更新CountDownComponent子组件中的count值; 250 2513. 更新count状态变量值也会触发CountDownComponent的重新渲染,在重新渲染过程中,评估使用count状态变量的if语句条件(this.count > 0),并执行true分支中的使用count状态变量的UI组件相关描述来更新Text组件的UI显示; 252 2534. 当按下子组件CountDownComponent的“Try again”按钮时,其\@Prop变量count将被更改,但是count值的更改不会影响父组件的countDownStartValue值; 254 2555. 父组件的countDownStartValue值会变化时,父组件的修改将覆盖掉子组件CountDownComponent中count本地的修改。 256 257 258### 父组件\@State数组项到子组件\@Prop简单数据类型同步 259 260 261父组件中\@State如果装饰的数组,其数组项也可以初始化\@Prop。以下示例中父组件Index中\@State装饰的数组arr,将其数组项初始化子组件Child中\@Prop装饰的value。 262 263 264 265```ts 266@Component 267struct Child { 268 @Prop value: number = 0; 269 270 build() { 271 Text(`${this.value}`) 272 .fontSize(50) 273 .onClick(()=>{this.value++}) 274 } 275} 276 277@Entry 278@Component 279struct Index { 280 @State arr: number[] = [1,2,3]; 281 282 build() { 283 Row() { 284 Column() { 285 Child({value: this.arr[0]}) 286 Child({value: this.arr[1]}) 287 Child({value: this.arr[2]}) 288 289 Divider().height(5) 290 291 ForEach(this.arr, 292 (item: number) => { 293 Child({value: item}) 294 }, 295 (item: string) => item.toString() 296 ) 297 Text('replace entire arr') 298 .fontSize(50) 299 .onClick(()=>{ 300 // 两个数组都包含项“3”。 301 this.arr = this.arr[0] == 1 ? [3,4,5] : [1,2,3]; 302 }) 303 } 304 } 305 } 306} 307``` 308 309 310初始渲染创建6个子组件实例,每个\@Prop装饰的变量初始化都在本地拷贝了一份数组项。子组件onclick事件处理程序会更改局部变量值。 311 312 313假设我们点击了多次,所有变量的本地取值都是“7”。 314 315 316 317``` 3187 3197 3207 321---- 3227 3237 3247 325``` 326 327 328单击replace entire arr后,屏幕将显示以下信息。 329 330 331 332``` 3333 3344 3355 336---- 3377 3384 3395 340``` 341 342 343- 在子组件Child中做的所有的修改都不会同步回父组件Index组件,所以即使6个组件显示都为7,但在父组件Index中,this.arr保存的值依旧是[1,2,3]。 344 345- 点击replace entire arr,this.arr[0] == 1成立,将this.arr赋值为[3, 4, 5]; 346 347- 因为this.arr[0]已更改,Child({value: this.arr[0]})组件将this.arr[0]更新同步到实例\@Prop装饰的变量。Child({value: this.arr[1]})和Child({value: this.arr[2]})的情况也类似。 348 349 350- this.arr的更改触发ForEach更新,this.arr更新的前后都有数值为3的数组项:[3, 4, 5] 和[1, 2, 3]。根据diff机制,数组项“3”将被保留,删除“1”和“2”的数组项,添加为“4”和“5”的数组项。这就意味着,数组项“3”的组件不会重新生成,而是将其移动到第一位。所以“3”对应的组件不会更新,此时“3”对应的组件数值为“7”,ForEach最终的渲染结果是“7”,“4”,“5”。 351 352 353### 从父组件中的\@State类对象属性到\@Prop简单类型的同步 354 355如果图书馆有一本图书和两位用户,每位用户都可以将图书标记为已读,此标记行为不会影响其它读者用户。从代码角度讲,对\@Prop图书对象的本地更改不会同步给图书馆组件中的\@State图书对象。 356 357在此示例中,图书类可以使用\@Observed装饰器,但不是必须的,只有在嵌套结构时需要此装饰器。这一点我们会在[从父组件中的@State数组项到@Prop class类型的同步](#从父组件中的state数组项到prop-class类型的同步)说明。 358 359 360```ts 361class Book { 362 public title: string; 363 public pages: number; 364 public readIt: boolean = false; 365 366 constructor(title: string, pages: number) { 367 this.title = title; 368 this.pages = pages; 369 } 370} 371 372@Component 373struct ReaderComp { 374 @Prop book: Book = new Book("", 0); 375 376 build() { 377 Row() { 378 Text(this.book.title) 379 Text(`...has${this.book.pages} pages!`) 380 Text(`...${this.book.readIt ? "I have read" : 'I have not read it'}`) 381 .onClick(() => this.book.readIt = true) 382 } 383 } 384} 385 386@Entry 387@Component 388struct Library { 389 @State book: Book = new Book('100 secrets of C++', 765); 390 391 build() { 392 Column() { 393 ReaderComp({ book: this.book }) 394 ReaderComp({ book: this.book }) 395 } 396 } 397} 398``` 399 400### 从父组件中的\@State数组项到\@Prop class类型的同步 401 402在下面的示例中,更改了\@State 修饰的allBooks数组中Book对象上的属性,但点击“Mark read for everyone”无反应。这是因为该属性是第二层的嵌套属性,\@State装饰器只能观察到第一层属性,不会观察到此属性更改,所以框架不会更新ReaderComp。 403 404```ts 405let nextId: number = 1; 406 407// @Observed 408class Book { 409 public id: number; 410 public title: string; 411 public pages: number; 412 public readIt: boolean = false; 413 414 constructor(title: string, pages: number) { 415 this.id = nextId++; 416 this.title = title; 417 this.pages = pages; 418 } 419} 420 421@Component 422struct ReaderComp { 423 @Prop book: Book = new Book(); 424 425 build() { 426 Row() { 427 Text(this.book.title) 428 Text(`...has${this.book.pages} pages!`) 429 Text(`...${this.book.readIt ? "I have read" : 'I have not read it'}`) 430 .onClick(() => this.book.readIt = true) 431 } 432 } 433} 434 435@Entry 436@Component 437struct Library { 438 @State allBooks: Book[] = [new Book("100 secrets of C++", 765), new Book("Effective C++", 651), new Book("The C++ programming language", 1765)]; 439 440 build() { 441 Column() { 442 Text('library`s all time favorite') 443 ReaderComp({ book: this.allBooks[2] }) 444 Divider() 445 Text('Books on loaan to a reader') 446 ForEach(this.allBooks, (book: void) => { 447 ReaderComp({ book: book }) 448 }, 449 (book: number): number => book.id) 450 Button('Add new') 451 .onClick(() => { 452 this.allBooks.push(new Book("The C++ Standard Library", 512)); 453 }) 454 Button('Remove first book') 455 .onClick(() => { 456 this.allBooks.shift(); 457 }) 458 Button("Mark read for everyone") 459 .onClick(() => { 460 this.allBooks.forEach((book) => book.readIt = true) 461 }) 462 } 463 } 464} 465``` 466 467 需要使用\@Observed装饰class Book,Book的属性将被观察。 需要注意的是,\@Prop在子组件装饰的状态变量和父组件的数据源是单向同步关系,即ReaderComp中的\@Prop book的修改不会同步给父组件Library。而父组件只会在数值有更新的时候(和上一次状态的对比),才会触发UI的重新渲染。 468 469```ts 470@Observed 471class Book { 472 public id: number; 473 public title: string; 474 public pages: number; 475 public readIt: boolean = false; 476 477 constructor(title: string, pages: number) { 478 this.id = nextId++; 479 this.title = title; 480 this.pages = pages; 481 } 482} 483``` 484 485\@Observed装饰的类的实例会被不透明的代理对象包装,此代理可以检测到包装对象内的所有属性更改。如果发生这种情况,此时,代理通知\@Prop,\@Prop对象值被更新。 486 487### \@Prop本地初始化不和父组件同步 488 489为了支持\@Component装饰的组件复用场景,\@Prop支持本地初始化,这样可以让\@Prop是否与父组件建立同步关系变得可选。当且仅当\@Prop有本地初始化时,从父组件向子组件传递\@Prop的数据源才是可选的。 490 491下面的示例中,子组件包含两个\@Prop变量: 492 493- \@Prop customCounter没有本地初始化,所以需要父组件提供数据源去初始化\@Prop,并当父组件的数据源变化时,\@Prop也将被更新; 494 495- \@Prop customCounter2有本地初始化,在这种情况下,\@Prop依旧允许但非强制父组件同步数据源给\@Prop。 496 497 498```ts 499@Component 500struct MyComponent { 501 @Prop customCounter: number = 0; 502 @Prop customCounter2: number = 5; 503 504 build() { 505 Column() { 506 Row() { 507 Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010') 508 } 509 510 Row() { 511 Button('Click to change locally !').width(180).height(60).margin({ top: 10 }) 512 .onClick(() => { 513 this.customCounter2++ 514 }) 515 }.height(100).width(180) 516 517 Row() { 518 Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010') 519 } 520 } 521 } 522} 523 524@Entry 525@Component 526struct MainProgram { 527 @State mainCounter: number = 10; 528 529 build() { 530 Column() { 531 Row() { 532 Column() { 533 Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 }) 534 .onClick(() => { 535 this.mainCounter++ 536 }) 537 } 538 } 539 540 Row() { 541 Column() { 542 // customCounter必须从父组件初始化,因为MyComponent的customCounter成员变量缺少本地初始化;此处,customCounter2可以不做初始化。 543 MyComponent({ customCounter: this.mainCounter }) 544 // customCounter2也可以从父组件初始化,父组件初始化的值会覆盖子组件customCounter2的本地初始化的值 545 MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter }) 546 } 547 } 548 } 549 } 550} 551``` 552 553### \@Prop嵌套场景 554 555在嵌套场景下,每一层都要用@Observed装饰,且每一层都要被@Prop接收,这样才能观察到嵌套场景。 556 557```ts 558// 以下是嵌套类对象的数据结构。 559@Observed 560class ClassA { 561 public title: string; 562 563 constructor(title: string) { 564 this.title = title; 565 } 566} 567 568@Observed 569class ClassB { 570 public name: string; 571 public a: ClassA; 572 573 constructor(name: string, a: ClassA) { 574 this.name = name; 575 this.a = a; 576 } 577} 578``` 579 580以下组件层次结构呈现的是@Prop嵌套场景的数据结构。 581 582```ts 583 584@Entry 585@Component 586struct Parent { 587 @State votes: ClassB = new ClassB('Hello', new ClassA('world')) 588 589 build() { 590 Column() { 591 Button('change') 592 .onClick(() => { 593 this.votes.name = "aaaaa" 594 this.votes.a.title = "wwwww" 595 }) 596 Child({ vote: this.votes }) 597 } 598 599 } 600} 601 602@Component 603struct Child { 604 @Prop vote: ClassB = new ClassB('', new ClassA('')); 605 build() { 606 Column() { 607 608 Text(this.vote.name).fontSize(36).fontColor(Color.Red).margin(50) 609 .onClick(() => { 610 this.vote.name = 'Bye' 611 }) 612 Text(this.vote.a.title).fontSize(36).fontColor(Color.Blue) 613 .onClick(() => { 614 this.vote.a.title = "openHarmony" 615 }) 616 Child1({vote1:this.vote.a}) 617 618 } 619 } 620} 621 622@Component 623struct Child1 { 624 @Prop vote1: ClassA = new ClassA(''); 625 build() { 626 Column() { 627 Text(this.vote1.title).fontSize(36).fontColor(Color.Red).margin(50) 628 .onClick(() => { 629 this.vote1.title = 'Bye Bye' 630 }) 631 } 632 } 633} 634``` 635 636## Prop支持联合类型实例 637 638@Prop支持联合类型和undefined和null,在下面的示例中,count类型为ClassA | undefined,点击父组件Library中的Button改变count的属性或者类型,Child中也会对应刷新。 639 640```ts 641class Animals { 642 public name: string; 643 644 constructor(name: string) { 645 this.name = name; 646 } 647} 648 649@Component 650struct Child { 651 @Prop animal: Animals | undefined; 652 653 build() { 654 Column() { 655 Text(`Child's animal is ${this.animal instanceof Animals ? this.animal.name : 'undefined'}`).fontSize(30) 656 657 Button('Child change animals into tigers') 658 .onClick(() => { 659 // 赋值为Animals的实例 660 this.animal = new Animals("Tiger") 661 }) 662 663 Button('Child change animal to undefined') 664 .onClick(() => { 665 // 赋值为undefined 666 this.animal = undefined 667 }) 668 669 }.width('100%') 670 } 671} 672 673@Entry 674@Component 675struct Library { 676 @State animal: Animals | undefined = new Animals("lion"); 677 678 build() { 679 Column() { 680 Text(`Parents' animals are ${this.animal instanceof Animals ? this.animal.name : 'undefined'}`).fontSize(30) 681 682 Child({animal: this.animal}) 683 684 Button('Parents change animals into dogs') 685 .onClick(() => { 686 // 判断animal的类型,做属性的更新 687 if (this.animal instanceof Animals) { 688 this.animal.name = "Dog" 689 } else { 690 console.info('num is undefined, cannot change property') 691 } 692 }) 693 694 Button('Parents change animal to undefined') 695 .onClick(() => { 696 // 赋值为undefined 697 this.animal = undefined 698 }) 699 } 700 } 701} 702``` 703 704<!--no_check-->