1# MVVM模式 2 3 4应用通过状态去渲染更新UI是程序设计中相对复杂,但又十分重要的,往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象,或者是嵌套对象组合而成。在这些情况下,ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。 5 6 7- Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。 8 9- View层:在ArkUI中通常是\@Components修饰组件渲染的UI。 10 11- ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。 12 - 自定义组件通过执行其build()方法或者\@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。 13 - View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了\@Watch回调方法用于监听状态数据的改变。 14 - 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。 15 - ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。 16 17 18目前很多关于UI构造和更新的问题,都是由于ViewModel的设计并没有很好的支持自定义组件的渲染,或者试图去让自定义组件强行适配Model层,而中间没有用ViewModel来进行分离。例如,一个应用程序直接将SQL数据库中的数据读入内存,这种数据模型不能很好的直接适配自定义组件的渲染,所以在应用程序开发中需要适配ViewModel层。 19 20 21 22 23 24根据上面涉及SQL数据库的示例,应用程序应设计为: 25 26 27- Model:针对数据库高效操作的数据模型。 28 29- ViewModel:针对ArkUI状态管理功能进行高效的UI更新的视图模型。 30 31- 部署 converters/adapters: converters/adapters作用于Model和ViewModel的相互转换。 32 - converters/adapters可以转换最初从数据库读取的Model,来创建并初始化ViewModel。 33 - 在应用的使用场景中,UI会通过event handler改变ViewModel,此时converters/adapters需要将ViewModel的更新数据同步回Model。 34 35 36虽然与强制将UI拟合到SQL数据库模式(MV模式)相比,MVVM的设计比较复杂,但应用程序开发人员可以通过ViewModel层的隔离,来简化UI的设计和实现,以此来收获更好的UI性能。 37 38 39## ViewModel的数据源 40 41 42ViewModel通常包含多个顶层数据源。\@State和\@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是: 43 44 45- \@State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。 46 47- \@Provide:组件级别的共享,可以通过key和\@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于\@State。 48 49- LocalStorage:页面级别的共享,可以通过\@Entry在当前组件树上共享LocalStorage实例。 50 51- AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。 52 53 54### \@State装饰的变量与一个或多个子组件共享状态数据 55 56 57\@State可以初始化多种状态变量,\@Prop、\@Link和\@ObjectLink可以和其建立单向或双向同步,详情见[@State使用规范](arkts-state.md)。 58 59 601. 使用Parent根节点中\@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。 61 62 ```ts 63 // xxx.ets 64 @Entry 65 @Component 66 struct Parent { 67 @State @Watch("testNumChange1") testNum: number = 1; 68 69 testNumChange1(propName: string): void { 70 console.log(`Parent: testNumChange value ${this.testNum}`) 71 } 72 73 build() { 74 Column() { 75 LinkChild({ testNum: $testNum }) 76 Sibling({ testNum: $testNum }) 77 } 78 } 79 } 80 ``` 81 822. LinkChild和Sibling中用\@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。 83 84 ```ts 85 @Component 86 struct Sibling { 87 @Link @Watch("testNumChange") testNum: number; 88 89 testNumChange(propName: string): void { 90 console.log(`Sibling: testNumChange value ${this.testNum}`); 91 } 92 93 build() { 94 Text(`Sibling: ${this.testNum}`) 95 } 96 } 97 98 @Component 99 struct LinkChild { 100 @Link @Watch("testNumChange") testNum: number; 101 102 testNumChange(propName: string): void { 103 console.log(`LinkChild: testNumChange value ${this.testNum}`); 104 } 105 106 build() { 107 Column() { 108 Button('incr testNum') 109 .onClick(() => { 110 console.log(`LinkChild: before value change value ${this.testNum}`); 111 this.testNum = this.testNum + 1 112 console.log(`LinkChild: after value change value ${this.testNum}`); 113 }) 114 Text(`LinkChild: ${this.testNum}`) 115 LinkLinkChild({ testNumGrand: $testNum }) 116 PropLinkChild({ testNumGrand: this.testNum }) 117 } 118 .height(200).width(200) 119 } 120 } 121 ``` 122 1233. LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的\@Prop和其父组件建立单向同步关系。 124 125 ```ts 126 @Component 127 struct LinkLinkChild { 128 @Link @Watch("testNumChange") testNumGrand: number; 129 130 testNumChange(propName: string): void { 131 console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); 132 } 133 134 build() { 135 Text(`LinkLinkChild: ${this.testNumGrand}`) 136 } 137 } 138 139 140 @Component 141 struct PropLinkChild { 142 @Prop @Watch("testNumChange") testNumGrand: number = 0; 143 144 testNumChange(propName: string): void { 145 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 146 } 147 148 build() { 149 Text(`PropLinkChild: ${this.testNumGrand}`) 150 .height(70) 151 .backgroundColor(Color.Red) 152 .onClick(() => { 153 this.testNumGrand += 1; 154 }) 155 } 156 } 157 ``` 158 159  160 161 当LinkChild中的\@Link testNum更改时。 162 163 1. 更改首先同步到其父组件Parent,然后更改从Parent同步到Sibling。 164 165 2. LinkChild中的\@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。 166 167 \@State装饰器与\@Provide、LocalStorage、AppStorage的区别: 168 169 - \@State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。 170 - 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。 171 172 完整的代码示例如下: 173 174 175 ```ts 176 @Component 177 struct LinkLinkChild { 178 @Link @Watch("testNumChange") testNumGrand: number; 179 180 testNumChange(propName: string): void { 181 console.log(`LinkLinkChild: testNumGrand value ${this.testNumGrand}`); 182 } 183 184 build() { 185 Text(`LinkLinkChild: ${this.testNumGrand}`) 186 } 187 } 188 189 190 @Component 191 struct PropLinkChild { 192 @Prop @Watch("testNumChange") testNumGrand: number = 0; 193 194 testNumChange(propName: string): void { 195 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 196 } 197 198 build() { 199 Text(`PropLinkChild: ${this.testNumGrand}`) 200 .height(70) 201 .backgroundColor(Color.Red) 202 .onClick(() => { 203 this.testNumGrand += 1; 204 }) 205 } 206 } 207 208 209 @Component 210 struct Sibling { 211 @Link @Watch("testNumChange") testNum: number; 212 213 testNumChange(propName: string): void { 214 console.log(`Sibling: testNumChange value ${this.testNum}`); 215 } 216 217 build() { 218 Text(`Sibling: ${this.testNum}`) 219 } 220 } 221 222 @Component 223 struct LinkChild { 224 @Link @Watch("testNumChange") testNum: number; 225 226 testNumChange(propName: string): void { 227 console.log(`LinkChild: testNumChange value ${this.testNum}`); 228 } 229 230 build() { 231 Column() { 232 Button('incr testNum') 233 .onClick(() => { 234 console.log(`LinkChild: before value change value ${this.testNum}`); 235 this.testNum = this.testNum + 1 236 console.log(`LinkChild: after value change value ${this.testNum}`); 237 }) 238 Text(`LinkChild: ${this.testNum}`) 239 LinkLinkChild({ testNumGrand: $testNum }) 240 PropLinkChild({ testNumGrand: this.testNum }) 241 } 242 .height(200).width(200) 243 } 244 } 245 246 247 @Entry 248 @Component 249 struct Parent { 250 @State @Watch("testNumChange1") testNum: number = 1; 251 252 testNumChange1(propName: string): void { 253 console.log(`Parent: testNumChange value ${this.testNum}`) 254 } 255 256 build() { 257 Column() { 258 LinkChild({ testNum: $testNum }) 259 Sibling({ testNum: $testNum }) 260 } 261 } 262 } 263 ``` 264 265 266### \@Provide装饰的变量与任何后代组件共享状态数据 267 268\@Provide装饰的变量可以与任何后代组件共享状态数据,其后代组件使用\@Consume创建双向同步,详情见[@Provide和@Consume](arkts-provide-and-consume.md)。 269 270因此,\@Provide-\@Consume模式比使用\@State-\@Link-\@Link从父组件将更改传递到孙子组件更方便。\@Provide-\@Consume适合在单个页面UI组件树中共享状态数据。 271 272使用\@Provide-\@Consume模式时,\@Consume和其祖先组件中的\@Provide通过绑定相同的key连接,而不是在组件的构造函数中通过参数来进行传递。 273 274以下示例通过\@Provide-\@Consume模式,将更改从父组件传递到孙子组件。 275 276 277```ts 278@Component 279struct LinkLinkChild { 280 @Consume @Watch("testNumChange") testNum: number; 281 282 testNumChange(propName: string): void { 283 console.log(`LinkLinkChild: testNum value ${this.testNum}`); 284 } 285 286 build() { 287 Text(`LinkLinkChild: ${this.testNum}`) 288 } 289} 290 291@Component 292struct PropLinkChild { 293 @Prop @Watch("testNumChange") testNumGrand: number = 0; 294 295 testNumChange(propName: string): void { 296 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 297 } 298 299 build() { 300 Text(`PropLinkChild: ${this.testNumGrand}`) 301 .height(70) 302 .backgroundColor(Color.Red) 303 .onClick(() => { 304 this.testNumGrand += 1; 305 }) 306 } 307} 308 309@Component 310struct Sibling { 311 @Consume @Watch("testNumChange") testNum: number; 312 313 testNumChange(propName: string): void { 314 console.log(`Sibling: testNumChange value ${this.testNum}`); 315 } 316 317 build() { 318 Text(`Sibling: ${this.testNum}`) 319 } 320} 321 322@Component 323struct LinkChild { 324 @Consume @Watch("testNumChange") testNum: number; 325 326 testNumChange(propName: string): void { 327 console.log(`LinkChild: testNumChange value ${this.testNum}`); 328 } 329 330 build() { 331 Column() { 332 Button('incr testNum') 333 .onClick(() => { 334 console.log(`LinkChild: before value change value ${this.testNum}`); 335 this.testNum = this.testNum + 1 336 console.log(`LinkChild: after value change value ${this.testNum}`); 337 }) 338 Text(`LinkChild: ${this.testNum}`) 339 LinkLinkChild({ /* empty */ }) 340 PropLinkChild({ testNumGrand: this.testNum }) 341 } 342 .height(200).width(200) 343 } 344} 345 346@Entry 347@Component 348struct Parent { 349 @Provide @Watch("testNumChange1") testNum: number = 1; 350 351 testNumChange1(propName: string): void { 352 console.log(`Parent: testNumChange value ${this.testNum}`) 353 } 354 355 build() { 356 Column() { 357 LinkChild({ /* empty */ }) 358 Sibling({ /* empty */ }) 359 } 360 } 361} 362``` 363 364 365### 给LocalStorage实例中对应的属性建立双向或单向同步 366 367通过\@LocalStorageLink和\@LocalStorageProp,给LocalStorage实例中的属性建立双向或单向同步。可以将LocalStorage实例视为\@State变量的Map,使用详情参考[LocalStorage](arkts-localstorage.md)。 368 369LocalStorage对象可以在ArkUI应用程序的几个页面上共享。因此,使用\@LocalStorageLink、\@LocalStorageProp和LocalStorage可以在应用程序的多个页面上共享状态。 370 371以下示例中: 372 3731. 创建一个LocalStorage实例,并通过\@Entry(storage)将其注入根节点。 374 3752. 在Parent组件中初始化\@LocalStorageLink("testNum")变量时,将在LocalStorage实例中创建testNum属性,并设置指定的初始值为1,即\@LocalStorageLink("testNum") testNum: number = 1。 376 3773. 在其子组件中,都使用\@LocalStorageLink或\@LocalStorageProp绑定同一个属性名key来传递数据。 378 379LocalStorage可以被认为是\@State变量的Map,属性名作为Map中的key。 380 381\@LocalStorageLink和LocalStorage中对应的属性的同步行为,和\@State和\@Link一致,都为双向数据同步。 382 383以下为组件的状态更新图: 384 385 386 387 388```ts 389@Component 390struct LinkLinkChild { 391 @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 392 393 testNumChange(propName: string): void { 394 console.log(`LinkLinkChild: testNum value ${this.testNum}`); 395 } 396 397 build() { 398 Text(`LinkLinkChild: ${this.testNum}`) 399 } 400} 401 402@Component 403struct PropLinkChild { 404 @LocalStorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; 405 406 testNumChange(propName: string): void { 407 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 408 } 409 410 build() { 411 Text(`PropLinkChild: ${this.testNumGrand}`) 412 .height(70) 413 .backgroundColor(Color.Red) 414 .onClick(() => { 415 this.testNumGrand += 1; 416 }) 417 } 418} 419 420@Component 421struct Sibling { 422 @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 423 424 testNumChange(propName: string): void { 425 console.log(`Sibling: testNumChange value ${this.testNum}`); 426 } 427 428 build() { 429 Text(`Sibling: ${this.testNum}`) 430 } 431} 432 433@Component 434struct LinkChild { 435 @LocalStorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 436 437 testNumChange(propName: string): void { 438 console.log(`LinkChild: testNumChange value ${this.testNum}`); 439 } 440 441 build() { 442 Column() { 443 Button('incr testNum') 444 .onClick(() => { 445 console.log(`LinkChild: before value change value ${this.testNum}`); 446 this.testNum = this.testNum + 1 447 console.log(`LinkChild: after value change value ${this.testNum}`); 448 }) 449 Text(`LinkChild: ${this.testNum}`) 450 LinkLinkChild({ /* empty */ }) 451 PropLinkChild({ /* empty */ }) 452 } 453 .height(200).width(200) 454 } 455} 456 457// create LocalStorage object to hold the data 458const storage = new LocalStorage(); 459@Entry(storage) 460@Component 461struct Parent { 462 @LocalStorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; 463 464 testNumChange1(propName: string): void { 465 console.log(`Parent: testNumChange value ${this.testNum}`) 466 } 467 468 build() { 469 Column() { 470 LinkChild({ /* empty */ }) 471 Sibling({ /* empty */ }) 472 } 473 } 474} 475``` 476 477 478### 给AppStorage中对应的属性建立双向或单向同步 479 480AppStorage是LocalStorage的单例对象,ArkUI在应用程序启动时创建该对象,在页面中使用\@StorageLink和\@StorageProp为多个页面之间共享数据,具体使用方法和LocalStorage类似。 481 482也可以使用PersistentStorage将AppStorage中的特定属性持久化到本地磁盘的文件中,再次启动的时候\@StorageLink和\@StorageProp会恢复上次应用退出的数据。详情请参考[PersistentStorage文档](arkts-persiststorage.md)。 483 484示例如下: 485 486 487```ts 488@Component 489struct LinkLinkChild { 490 @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 491 492 testNumChange(propName: string): void { 493 console.log(`LinkLinkChild: testNum value ${this.testNum}`); 494 } 495 496 build() { 497 Text(`LinkLinkChild: ${this.testNum}`) 498 } 499} 500 501@Component 502struct PropLinkChild { 503 @StorageProp("testNum") @Watch("testNumChange") testNumGrand: number = 1; 504 505 testNumChange(propName: string): void { 506 console.log(`PropLinkChild: testNumGrand value ${this.testNumGrand}`); 507 } 508 509 build() { 510 Text(`PropLinkChild: ${this.testNumGrand}`) 511 .height(70) 512 .backgroundColor(Color.Red) 513 .onClick(() => { 514 this.testNumGrand += 1; 515 }) 516 } 517} 518 519@Component 520struct Sibling { 521 @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 522 523 testNumChange(propName: string): void { 524 console.log(`Sibling: testNumChange value ${this.testNum}`); 525 } 526 527 build() { 528 Text(`Sibling: ${this.testNum}`) 529 } 530} 531 532@Component 533struct LinkChild { 534 @StorageLink("testNum") @Watch("testNumChange") testNum: number = 1; 535 536 testNumChange(propName: string): void { 537 console.log(`LinkChild: testNumChange value ${this.testNum}`); 538 } 539 540 build() { 541 Column() { 542 Button('incr testNum') 543 .onClick(() => { 544 console.log(`LinkChild: before value change value ${this.testNum}`); 545 this.testNum = this.testNum + 1 546 console.log(`LinkChild: after value change value ${this.testNum}`); 547 }) 548 Text(`LinkChild: ${this.testNum}`) 549 LinkLinkChild({ /* empty */ 550 }) 551 PropLinkChild({ /* empty */ 552 }) 553 } 554 .height(200).width(200) 555 } 556} 557 558 559@Entry 560@Component 561struct Parent { 562 @StorageLink("testNum") @Watch("testNumChange1") testNum: number = 1; 563 564 testNumChange1(propName: string): void { 565 console.log(`Parent: testNumChange value ${this.testNum}`) 566 } 567 568 build() { 569 Column() { 570 LinkChild({ /* empty */ 571 }) 572 Sibling({ /* empty */ 573 }) 574 } 575 } 576} 577``` 578 579 580## ViewModel的嵌套场景 581 582 583大多数情况下,ViewModel数据项都是复杂类型的,例如,对象数组、嵌套对象或者这些类型的组合。对于嵌套场景,可以使用\@Observed搭配\@Prop或者\@ObjectLink来观察变化。 584 585 586### \@Prop和\@ObjectLink嵌套数据结构 587 588推荐设计单独的\@Component来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个\@Component,一个\@Component呈现外部数组/对象,另一个\@Component呈现嵌套在数组/对象内的类对象。 \@Prop、\@Link、\@ObjectLink修饰的变量只能观察到第一层的变化。 589 590- 对于类: 591 - 可以观察到赋值的变化:this.obj=new ClassObj(...) 592 - 可以观察到对象属性的更改:this.obj.a=new ClassA(...) 593 - 不能观察更深层级的属性更改:this.obj.a.b = 47 594 595- 对于数组: 596 - 可以观察到数组的整体赋值:this.arr=[...] 597 - 可以观察到数据项的删除、插入和替换:this.arr[1] = new ClassA()、this.arr.pop()、 this.arr.push(new ClassA(...))、this.arr.sort(...) 598 - 不能观察更深层级的数组变化:this.arr[1].b = 47 599 600如果要观察嵌套类的内部对象的变化,可以使用\@ObjectLink或\@Prop。优先考虑\@ObjectLink,其通过嵌套对象内部属性的引用初始化自身。\@Prop会对嵌套在内部的对象的深度拷贝来进行初始化,以实现单向同步。在性能上\@Prop的深度拷贝比\@ObjectLink的引用拷贝慢很多。 601 602\@ObjectLink或\@Prop可以用来存储嵌套内部的类对象,该类必须用\@Observed类装饰器装饰,否则类的属性改变并不会触发更新,UI并不会刷新。\@Observed为其装饰的类实现自定义构造函数,此构造函数创建了一个类的实例,并使用ES6代理包装(由ArkUI框架实现),拦截修饰class属性的所有“get”和“set”。“set”观察属性值,当发生赋值操作时,通知ArkUI框架更新。“get”收集哪些UI组件依赖该状态变量,实现最小化UI更新。 603 604如果嵌套场景中,嵌套数据内部是数组或者class时,需根据以下场景使用\@Observed类装饰器。 605 606- 如果嵌套数据内部是class,直接被\@Observed装饰。 607 608- 如果嵌套数据内部是数组,可以通过以下方式来观察数组变化。 609 610 ```ts 611 @Observed class ObservedArray<T> extends Array<T> { 612 constructor(args: T[]) { 613 if (args instanceof Array) { 614 super(...args); 615 } else { 616 super(args) 617 } 618 } 619 /* otherwise empty */ 620 } 621 ``` 622 623 ViewModel为外层class。 624 625 626 ```ts 627 class Outer { 628 innerArrayProp : ObservedArray<string> = []; 629 ... 630 } 631 ``` 632 633 634### 嵌套数据结构中\@Prop和\@ObjectLink之的区别 635 636以下示例中: 637 638- 父组件ViewB渲染\@State arrA:Array<ClassA>。\@State可以观察新数组的分配、数组项插入、删除和替换。 639 640- 子组件ViewA渲染每一个ClassA的对象。 641 642- 类装饰器\@Observed ClassA与\@ObjectLink a: ClassA。 643 644 - 可以观察嵌套在Array内的ClassA对象的变化。 645 646 - 不使用\@Observed时: 647 ViewB中的this.arrA[Math.floor(this.arrA.length/2)].c=10将不会被观察到,相应的ViewA组件也不会更新。 648 649 对于数组中的第一个和第二个数组项,每个数组项都初始化了两个ViewA的对象,渲染了同一个ViewA实例。在一个ViewA中的属性赋值this.a.c += 1;时不会引发另外一个使用同一个ClassA初始化的ViewA的渲染更新。 650 651 652 653 654```ts 655let NextID: number = 1; 656 657// 类装饰器@Observed装饰ClassA 658@Observed 659class ClassA { 660 public id: number; 661 public c: number; 662 663 constructor(c: number) { 664 this.id = NextID++; 665 this.c = c; 666 } 667} 668 669@Component 670struct ViewA { 671 @ObjectLink a: ClassA; 672 label: string = "ViewA1"; 673 674 build() { 675 Row() { 676 Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) 677 .onClick(() => { 678 // 改变对象属性 679 this.a.c += 1; 680 }) 681 } 682 } 683} 684 685@Entry 686@Component 687struct ViewB { 688 @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; 689 690 build() { 691 Column() { 692 ForEach(this.arrA, 693 (item: ClassA) => { 694 ViewA({ label: `#${item.id}`, a: item }) 695 }, 696 (item: ClassA): string => { return item.id.toString(); } 697 ) 698 699 Divider().height(10) 700 701 if (this.arrA.length) { 702 ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) 703 ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) 704 } 705 706 Divider().height(10) 707 708 Button(`ViewB: reset array`) 709 .onClick(() => { 710 // 替换整个数组,会被@State this.arrA观察到 711 this.arrA = [new ClassA(0), new ClassA(0)]; 712 }) 713 Button(`array push`) 714 .onClick(() => { 715 // 数组中插入数据,会被@State this.arrA观察到 716 this.arrA.push(new ClassA(0)) 717 }) 718 Button(`array shift`) 719 .onClick(() => { 720 // 数组中移除数据,会被@State this.arrA观察到 721 this.arrA.shift() 722 }) 723 Button(`ViewB: chg item property in middle`) 724 .onClick(() => { 725 // 替换数组中的某个元素,会被@State this.arrA观察到 726 this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); 727 }) 728 Button(`ViewB: chg item property in middle`) 729 .onClick(() => { 730 // 改变数组中某个元素的属性c,会被ViewA中的@ObjectLink观察到 731 this.arrA[Math.floor(this.arrA.length / 2)].c = 10; 732 }) 733 } 734 } 735} 736``` 737 738在ViewA中,将\@ObjectLink替换为\@Prop。 739 740 741```ts 742@Component 743struct ViewA { 744 745 @Prop a: ClassA = new ClassA(0); 746 label : string = "ViewA1"; 747 748 build() { 749 Row() { 750 Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`) 751 .onClick(() => { 752 // change object property 753 this.a.c += 1; 754 }) 755 } 756 } 757} 758``` 759 760与用\@Prop修饰不同,用\@ObjectLink修饰时,点击数组的第一个或第二个元素,后面两个ViewA会发生同步的变化。 761 762\@Prop是单向数据同步,ViewA内的Button只会触发Button自身的刷新,不会传播到其他的ViewA实例中。在ViewA中的ClassA只是一个副本,并不是其父组件中\@State arrA : Array<ClassA>中的对象,也不是其他ViewA的ClassA,这使得数组的元素和ViewA中的元素表面是传入的同一个对象,实际上在UI上渲染使用的是两个互不相干的对象。 763 764需要注意\@Prop和\@ObjectLink还有一个区别:\@ObjectLink装饰的变量是仅可读的,不能被赋值;\@Prop装饰的变量可以被赋值。 765 766- \@ObjectLink实现双向同步,因为它是通过数据源的引用初始化的。 767 768- \@Prop是单向同步,需要深拷贝数据源。 769 770- 对于\@Prop赋值新的对象,就是简单地将本地的值覆写,但是对于实现双向数据同步的\@ObjectLink,覆写新的对象相当于要更新数据源中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。 771 772 773## MVVM应用示例 774 775 776以下示例深入探讨了嵌套ViewModel的应用程序设计,特别是自定义组件如何渲染一个嵌套的Object,该场景在实际的应用开发中十分常见。 777 778 779开发一个电话簿应用,实现功能如下: 780 781 782- 显示联系人和设备("Me")电话号码 。 783 784- 选中联系人时,进入可编辑态“Edit”,可以更新该联系人详细信息,包括电话号码,住址。 785 786- 在更新联系人信息时,只有在单击保存“Save Changes”之后,才会保存更改。 787 788- 可以点击删除联系人“Delete Contact”,可以在联系人列表删除该联系人。 789 790 791ViewModel需要包括: 792 793 794- AddressBook(class) 795 - me(设备): 存储一个Person类。 796 - contacts(设备联系人):存储一个Person类数组。 797 798 799AddressBook类声明如下: 800 801 802 803```ts 804export class AddressBook { 805 me: Person; 806 contacts: ObservedArray<Person>; 807 808 constructor(me: Person, contacts: Person[]) { 809 this.me = me; 810 this.contacts = new ObservedArray<Person>(contacts); 811 } 812} 813``` 814 815 816- Person (class) 817 - name : string 818 - address : Address 819 - phones: ObservedArray<string> 820 - Address (class) 821 - street : string 822 - zip : number 823 - city : string 824 825 826Address类声明如下: 827 828 829 830```ts 831@Observed 832export class Address { 833 street: string; 834 zip: number; 835 city: string; 836 837 constructor(street: string, 838 zip: number, 839 city: string) { 840 this.street = street; 841 this.zip = zip; 842 this.city = city; 843 } 844} 845``` 846 847 848Person类声明如下: 849 850 851 852```ts 853let nextId = 0; 854 855@Observed 856export class Person { 857 id_: string; 858 name: string; 859 address: Address; 860 phones: ObservedArray<string>; 861 862 constructor(name: string, 863 street: string, 864 zip: number, 865 city: string, 866 phones: string[]) { 867 this.id_ = `${nextId}`; 868 nextId++; 869 this.name = name; 870 this.address = new Address(street, zip, city); 871 this.phones = new ObservedArray<string>(phones); 872 } 873} 874``` 875 876 877需要注意的是,因为phones是嵌套属性,如果要观察到phones的变化,需要extends array,并用\@Observed修饰它。ObservedArray类的声明如下。 878 879 880 881```ts 882@Observed 883export class ObservedArray<T> extends Array<T> { 884 constructor(args: T[]) { 885 console.log(`ObservedArray: ${JSON.stringify(args)} `) 886 if (args instanceof Array) { 887 super(...args); 888 } else { 889 super(args) 890 } 891 } 892} 893``` 894 895 896- selected : 对Person的引用。 897 898 899更新流程如下: 900 901 9021. 在根节点PageEntry中初始化所有的数据,将me和contacts和其子组件AddressBookView建立双向数据同步,selectedPerson默认为me,需要注意,selectedPerson并不是PageEntry数据源中的数据,而是数据源中,对某一个Person的引用。 903 PageEntry和AddressBookView声明如下: 904 905 906 ```ts 907 @Component 908 struct AddressBookView { 909 910 @ObjectLink me : Person; 911 @ObjectLink contacts : ObservedArray<Person>; 912 @State selectedPerson: Person = new Person("", "", 0, "", []); 913 914 aboutToAppear() { 915 this.selectedPerson = this.me; 916 } 917 918 build() { 919 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start}) { 920 Text("Me:") 921 PersonView({ 922 person: this.me, 923 phones: this.me.phones, 924 selectedPerson: this.selectedPerson 925 }) 926 927 Divider().height(8) 928 929 ForEach(this.contacts, (contact: Person) => { 930 PersonView({ 931 person: contact, 932 phones: contact.phones as ObservedArray<string>, 933 selectedPerson: this.selectedPerson 934 }) 935 }, 936 (contact: Person): string => { return contact.id_; } 937 ) 938 939 Divider().height(8) 940 941 Text("Edit:") 942 PersonEditView({ 943 selectedPerson: this.selectedPerson, 944 name: this.selectedPerson.name, 945 address: this.selectedPerson.address, 946 phones: this.selectedPerson.phones 947 }) 948 } 949 .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) 950 } 951 } 952 953 @Entry 954 @Component 955 struct PageEntry { 956 @Provide addrBook: AddressBook = new AddressBook( 957 new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]), 958 [ 959 new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 960 new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 961 new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 962 ]); 963 964 build() { 965 Column() { 966 AddressBookView({ 967 me: this.addrBook.me, 968 contacts: this.addrBook.contacts, 969 selectedPerson: this.addrBook.me 970 }) 971 } 972 } 973 } 974 ``` 975 9762. PersonView,即电话簿中联系人姓名和首选电话的View,当用户选中,即高亮当前Person,需要同步回其父组件AddressBookView的selectedPerson,所以需要通过\@Link建立双向同步。 977 PersonView声明如下: 978 979 980 ```ts 981 // 显示联系人姓名和首选电话 982 // 为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, 983 // 显示首选号码不能使用this.person.phones[0],因为@ObjectLink person只代理了Person的属性,数组内部的变化观察不到 984 // 触发onClick事件更新selectedPerson 985 @Component 986 struct PersonView { 987 988 @ObjectLink person : Person; 989 @ObjectLink phones : ObservedArray<string>; 990 991 @Link selectedPerson : Person; 992 993 build() { 994 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 995 Text(this.person.name) 996 if (this.phones.length > 0) { 997 Text(this.phones[0]) 998 } 999 } 1000 .height(55) 1001 .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") 1002 .onClick(() => { 1003 this.selectedPerson = this.person; 1004 }) 1005 } 1006 } 1007 ``` 1008 10093. 选中的Person会在PersonEditView中显示详细信息,对于PersonEditView的数据同步分为以下三种方式: 1010 1011 - 在Edit状态通过Input.onChange回调事件接受用户的键盘输入时,在点击“Save Changes”之前,这个修改是不希望同步回数据源的,但又希望刷新在当前的PersonEditView中,所以\@Prop深拷贝当前Person的详细信息; 1012 1013 - PersonEditView通过\@Link seletedPerson: Person和AddressBookView的``selectedPerson建立双向同步,当用户点击“Save Changes”的时候,\@Prop的修改将被赋值给\@Link seletedPerson: Person,这就意味这,数据将被同步回数据源。 1014 1015 - PersonEditView中通过\@Consume addrBook: AddressBook和根节点PageEntry建立跨组件层级的直接的双向同步关系,当用户在PersonEditView界面删除某一个联系人时,会直接同步回PageEntry,PageEntry的更新会通知AddressBookView刷新contracts的列表页。 PersonEditView声明如下: 1016 1017 ```ts 1018 // 渲染Person的详细信息 1019 // @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 1020 // 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 1021 @Component 1022 struct PersonEditView { 1023 1024 @Consume addrBook : AddressBook; 1025 1026 /* 指向父组件selectedPerson的引用 */ 1027 @Link selectedPerson: Person; 1028 1029 /*在本地副本上编辑,直到点击保存*/ 1030 @Prop name: string = ""; 1031 @Prop address : Address = new Address("", 0, ""); 1032 @Prop phones : ObservedArray<string> = []; 1033 1034 selectedPersonIndex() : number { 1035 return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_); 1036 } 1037 1038 build() { 1039 Column() { 1040 TextInput({ text: this.name}) 1041 .onChange((value) => { 1042 this.name = value; 1043 }) 1044 TextInput({text: this.address.street}) 1045 .onChange((value) => { 1046 this.address.street = value; 1047 }) 1048 1049 TextInput({text: this.address.city}) 1050 .onChange((value) => { 1051 this.address.city = value; 1052 }) 1053 1054 TextInput({text: this.address.zip.toString()}) 1055 .onChange((value) => { 1056 const result = Number.parseInt(value); 1057 this.address.zip= Number.isNaN(result) ? 0 : result; 1058 }) 1059 1060 if (this.phones.length > 0) { 1061 ForEach(this.phones, 1062 (phone: ResourceStr, index?:number) => { 1063 TextInput({ text: phone }) 1064 .width(150) 1065 .onChange((value) => { 1066 console.log(`${index}. ${value} value has changed`) 1067 this.phones[index!] = value; 1068 }) 1069 }, 1070 (phone: ResourceStr, index?:number) => `${index}` 1071 ) 1072 } 1073 1074 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 1075 Text("Save Changes") 1076 .onClick(() => { 1077 // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 1078 // 避免创建新对象,在现有属性上进行修改 1079 this.selectedPerson.name = this.name; 1080 this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city) 1081 this.phones.forEach((phone : string, index : number) => { this.selectedPerson.phones[index] = phone } ); 1082 }) 1083 if (this.selectedPersonIndex()!=-1) { 1084 Text("Delete Contact") 1085 .onClick(() => { 1086 let index = this.selectedPersonIndex(); 1087 console.log(`delete contact at index ${index}`); 1088 1089 // 删除当前联系人 1090 this.addrBook.contacts.splice(index, 1); 1091 1092 // 删除当前selectedPerson,选中态前移一位 1093 index = (index < this.addrBook.contacts.length) ? index : index-1; 1094 1095 // 如果contract被删除完,则设置me为选中态 1096 this.selectedPerson = (index>=0) ? this.addrBook.contacts[index] : this.addrBook.me; 1097 }) 1098 } 1099 } 1100 1101 } 1102 } 1103 } 1104 ``` 1105 1106 其中关于\@ObjectLink和\@Link的区别要注意以下几点: 1107 1108 1. 在AddressBookView中实现和父组件PageView的双向同步,需要用\@ObjectLink me : Person和\@ObjectLink contacts : ObservedArray<Person>,而不能用\@Link,原因如下: 1109 - \@Link需要和其数据源类型完全相同,且仅能观察到第一层的变化; 1110 - \@ObjectLink可以被数据源的属性初始化,且代理了\@Observed装饰类的属性,可以观察到被装饰类属性的变化。 1111 2. 当 联系人姓名 (Person.name) 或者首选电话号码 (Person.phones[0]) 发生更新时,PersonView也需要同步刷新,其中Person.phones[0]属于第二层的更新,如果使用\@Link将无法观察到,而且\@Link需要和其数据源类型完全相同。所以在PersonView中也需要使用\@ObjectLink,即\@ObjectLink person : Person和\@ObjectLink phones : ObservedArray<string>。 1112 1113  1114 1115 在这个例子中,我们可以大概了解到如何构建ViewModel,在应用的根节点中,ViewModel的数据可能是可以巨大的嵌套数据,但是在ViewModel和View的适配和渲染中,我们尽可能将ViewModel的数据项和View相适配,这样的话在针对每一层的View,都是一个相对“扁平”的数据,仅观察当前层就可以了。 1116 1117 在应用实际开发中,也许我们无法避免去构建一个十分庞大的Model,但是我们可以在UI树状结构中合理地去拆分数据,使得ViewModel和View更好的适配,从而搭配最小化更新来实现高性能开发。 1118 1119 完整应用代码如下: 1120 1121 1122```ts 1123 1124// ViewModel classes 1125let nextId = 0; 1126 1127@Observed 1128export class ObservedArray<T> extends Array<T> { 1129 constructor(args: T[]) { 1130 console.log(`ObservedArray: ${JSON.stringify(args)} `) 1131 if (args instanceof Array) { 1132 super(...args); 1133 } else { 1134 super(args) 1135 } 1136 } 1137} 1138 1139@Observed 1140export class Address { 1141 street: string; 1142 zip: number; 1143 city: string; 1144 1145 constructor(street: string, 1146 zip: number, 1147 city: string) { 1148 this.street = street; 1149 this.zip = zip; 1150 this.city = city; 1151 } 1152} 1153 1154@Observed 1155export class Person { 1156 id_: string; 1157 name: string; 1158 address: Address; 1159 phones: ObservedArray<string>; 1160 1161 constructor(name: string, 1162 street: string, 1163 zip: number, 1164 city: string, 1165 phones: string[]) { 1166 this.id_ = `${nextId}`; 1167 nextId++; 1168 this.name = name; 1169 this.address = new Address(street, zip, city); 1170 this.phones = new ObservedArray<string>(phones); 1171 } 1172} 1173 1174export class AddressBook { 1175 me: Person; 1176 contacts: ObservedArray<Person>; 1177 1178 constructor(me: Person, contacts: Person[]) { 1179 this.me = me; 1180 this.contacts = new ObservedArray<Person>(contacts); 1181 } 1182} 1183 1184// 渲染出Person对象的名称和Observed数组<string>中的第一个号码 1185// 为了更新电话号码,这里需要@ObjectLink person和@ObjectLink phones, 1186// 不能使用this.person.phones,内部数组的更改不会被观察到。 1187// 在AddressBookView、PersonEditView中的onClick更新selectedPerson 1188@Component 1189struct PersonView { 1190 @ObjectLink person: Person; 1191 @ObjectLink phones: ObservedArray<string>; 1192 @Link selectedPerson: Person; 1193 1194 build() { 1195 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 1196 Text(this.person.name) 1197 if (this.phones.length) { 1198 Text(this.phones[0]) 1199 } 1200 } 1201 .height(55) 1202 .backgroundColor(this.selectedPerson.name == this.person.name ? "#ffa0a0" : "#ffffff") 1203 .onClick(() => { 1204 this.selectedPerson = this.person; 1205 }) 1206 } 1207} 1208 1209// 渲染Person的详细信息 1210// @Prop装饰的变量从父组件AddressBookView深拷贝数据,将变化保留在本地, TextInput的变化只会在本地副本上进行修改。 1211// 点击 "Save Changes" 会将所有数据的复制通过@Prop到@Link, 同步到其他组件 1212@Component 1213struct PersonEditView { 1214 @Consume addrBook: AddressBook; 1215 1216 /* 指向父组件selectedPerson的引用 */ 1217 @Link selectedPerson: Person; 1218 1219 /*在本地副本上编辑,直到点击保存*/ 1220 @Prop name: string = ""; 1221 @Prop address: Address = new Address("", 0, ""); 1222 @Prop phones: ObservedArray<string> = []; 1223 1224 selectedPersonIndex(): number { 1225 return this.addrBook.contacts.findIndex((person: Person) => person.id_ == this.selectedPerson.id_); 1226 } 1227 1228 build() { 1229 Column() { 1230 TextInput({ text: this.name }) 1231 .onChange((value) => { 1232 this.name = value; 1233 }) 1234 TextInput({ text: this.address.street }) 1235 .onChange((value) => { 1236 this.address.street = value; 1237 }) 1238 1239 TextInput({ text: this.address.city }) 1240 .onChange((value) => { 1241 this.address.city = value; 1242 }) 1243 1244 TextInput({ text: this.address.zip.toString() }) 1245 .onChange((value) => { 1246 const result = Number.parseInt(value); 1247 this.address.zip = Number.isNaN(result) ? 0 : result; 1248 }) 1249 1250 if (this.phones.length > 0) { 1251 ForEach(this.phones, 1252 (phone: ResourceStr, index?:number) => { 1253 TextInput({ text: phone }) 1254 .width(150) 1255 .onChange((value) => { 1256 console.log(`${index}. ${value} value has changed`) 1257 this.phones[index!] = value; 1258 }) 1259 }, 1260 (phone: ResourceStr, index?:number) => `${index}` 1261 ) 1262 } 1263 1264 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { 1265 Text("Save Changes") 1266 .onClick(() => { 1267 // 将本地副本更新的值赋值给指向父组件selectedPerson的引用 1268 // 避免创建新对象,在现有属性上进行修改 1269 this.selectedPerson.name = this.name; 1270 this.selectedPerson.address = new Address(this.address.street, this.address.zip, this.address.city) 1271 this.phones.forEach((phone: string, index: number) => { 1272 this.selectedPerson.phones[index] = phone 1273 }); 1274 }) 1275 if (this.selectedPersonIndex() != -1) { 1276 Text("Delete Contact") 1277 .onClick(() => { 1278 let index = this.selectedPersonIndex(); 1279 console.log(`delete contact at index ${index}`); 1280 1281 // 删除当前联系人 1282 this.addrBook.contacts.splice(index, 1); 1283 1284 // 删除当前selectedPerson,选中态前移一位 1285 index = (index < this.addrBook.contacts.length) ? index : index - 1; 1286 1287 // 如果contract被删除完,则设置me为选中态 1288 this.selectedPerson = (index >= 0) ? this.addrBook.contacts[index] : this.addrBook.me; 1289 }) 1290 } 1291 } 1292 1293 } 1294 } 1295} 1296 1297@Component 1298struct AddressBookView { 1299 @ObjectLink me: Person; 1300 @ObjectLink contacts: ObservedArray<Person>; 1301 @State selectedPerson: Person = new Person("", "", 0, "", []); 1302 1303 aboutToAppear() { 1304 this.selectedPerson = this.me; 1305 } 1306 1307 build() { 1308 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start }) { 1309 Text("Me:") 1310 PersonView({ 1311 person: this.me, 1312 phones: this.me.phones, 1313 selectedPerson: this.selectedPerson 1314 }) 1315 1316 Divider().height(8) 1317 1318 ForEach(this.contacts, (contact: Person) => { 1319 PersonView({ 1320 person: contact, 1321 phones: contact.phones as ObservedArray<string>, 1322 selectedPerson: this.selectedPerson 1323 }) 1324 }, 1325 (contact: Person): string => { return contact.id_; } 1326 ) 1327 1328 Divider().height(8) 1329 1330 Text("Edit:") 1331 PersonEditView({ 1332 selectedPerson: this.selectedPerson, 1333 name: this.selectedPerson.name, 1334 address: this.selectedPerson.address, 1335 phones: this.selectedPerson.phones 1336 }) 1337 } 1338 .borderStyle(BorderStyle.Solid).borderWidth(5).borderColor(0xAFEEEE).borderRadius(5) 1339 } 1340} 1341 1342@Entry 1343@Component 1344struct PageEntry { 1345 @Provide addrBook: AddressBook = new AddressBook( 1346 new Person("Gigi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********", "18*********"]), 1347 [ 1348 new Person("Oly", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 1349 new Person("Sam", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 1350 new Person("Vivi", "Itamerenkatu 9", 180, "Helsinki", ["18*********", "18*********"]), 1351 ]); 1352 1353 build() { 1354 Column() { 1355 AddressBookView({ 1356 me: this.addrBook.me, 1357 contacts: this.addrBook.contacts, 1358 selectedPerson: this.addrBook.me 1359 }) 1360 } 1361 } 1362} 1363```