• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![zh-cn_image_0000001653986573](figures/zh-cn_image_0000001653986573.png)
22
23
24根据上面涉及SQL数据库的示例,应用程序应设计为:
25
26
27- Model:针对数据库高效操作的数据模型。
28
29- ViewModel:针对ArkUI状态管理功能进行高效的UI更新的视图模型。
30
31- 部署 converters/adaptersconverters/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   ![zh-cn_image_0000001638250945](figures/zh-cn_image_0000001638250945.png)
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![zh-cn_image_0000001588450934](figures/zh-cn_image_0000001588450934.png)
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&lt;ClassA&gt;。\@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![zh-cn_image_0000001588610894](figures/zh-cn_image_0000001588610894.png)
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&lt;ClassA&gt;中的对象,也不是其他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&lt;string&gt;
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&lt;Person&gt;,而不能用\@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&lt;string&gt;。
1112
1113     ![zh-cn_image_0000001605293914](figures/zh-cn_image_0000001605293914.png)
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```