1# makeObserved接口:将非观察数据变为可观察数据
2
3为了将普通不可观察数据变为可观察数据,开发者可以使用[makeObserved接口](../reference/apis-arkui/js-apis-StateManagement.md#makeobserved)。
4
5
6makeObserved可以在\@Trace无法标记的情况下使用。在阅读本文档前,建议提前阅读:[\@Trace](./arkts-new-observedV2-and-trace.md)。
7
8>**说明:**
9>
10>从API version 12开始,开发者可以使用UIUtils中的makeObserved接口将普通不可观察数据变为可观察数据。
11
12## 概述
13
14- 状态管理框架已提供[@ObservedV2/@Trace](./arkts-new-observedV2-and-trace.md)用于观察类属性变化,makeObserved接口提供主要应用于@ObservedV2/@Trace无法涵盖的场景:
15
16  - class的定义在三方包中:开发者无法手动对class中需要观察的属性加上@Trace标签,可以使用makeObserved使得当前对象可以被观察。
17
18  - 当前类的成员属性不能被修改:因为@Trace观察类属性会动态修改类的属性,这个行为在@Sendable装饰的class中是不被允许的,此时可以使用makeObserved。
19
20  - interface或者JSON.parse返回的匿名对象:这类场景往往没有明确的class声明,开发者无法使用@Trace标记当前属性可以被观察,此时可以使用makeObserved。
21
22
23- 使用makeObserved接口需要导入UIUtils。
24  ```ts
25  import { UIUtils } from '@kit.ArkUI';
26  ```
27
28## 限制条件
29
30- makeObserved仅支持非空的对象类型传参。
31  - 不支持undefined和null:返回自身,不做任何处理。
32  - 非Object类型:编译拦截报错
33
34  ```ts
35  import { UIUtils } from '@kit.ArkUI';
36  let res1 = UIUtils.makeObserved(2); // 非法类型入参,错误用法,编译报错
37  let res2 = UIUtils.makeObserved(undefined); // 非法类型入参,错误用法,返回自身,res2 === undefined
38  let res3 = UIUtils.makeObserved(null); // 非法类型入参,错误用法,返回自身,res3 === null
39
40  class Info {
41    id: number = 0;
42  }
43  let rawInfo: Info = UIUtils.makeObserved(new Info()); // 正确用法
44  ```
45
46- makeObserved不支持传入被[@ObservedV2](./arkts-new-observedV2-and-trace.md)、[@Observed](./arkts-observed-and-objectlink.md)装饰的类的实例以及已经被makeObserved封装过的代理数据。为了防止双重代理,makeObserved发现入参为上述情况时则直接返回,不做处理。
47  ```ts
48  import { UIUtils } from '@kit.ArkUI';
49  @ObservedV2
50  class Info {
51    @Trace id: number = 0;
52  }
53  // 错误用法:makeObserved发现传入的实例是@ObservedV2装饰的类的实例,则返回传入对象自身
54  let observedInfo: Info = UIUtils.makeObserved(new Info());
55
56  class Info2 {
57    id: number = 0;
58  }
59  // 正确用法:传入对象既不是@ObservedV2/@Observed装饰的类的实例,也不是makeObserved封装过的代理数据
60  // 返回可观察数据
61  let observedInfo1: Info2 = UIUtils.makeObserved(new Info2());
62  // 错误用法:传入对象为makeObserved封装过的代理数据,此次makeObserved不做处理
63  let observedInfo2: Info2 = UIUtils.makeObserved(observedInfo1);
64  ```
65- makeObserved可以用在@Component装饰的自定义组件中,但不能和状态管理V1的状态变量装饰器配合使用,如果一起使用,则会抛出运行时异常。
66  ```ts
67  // 错误写法,运行时异常
68  @State message: Info = UIUtils.makeObserved(new Info(20));
69  ```
70  下面`message2`的写法不会抛异常,原因是this.message是@State装饰的,其实现等同于@Observed,而UIUtils.makeObserved的入参是@Observed装饰的class,会直接返回自身。因此对于`message2`来说,他的初始值不是makeObserved的返回值,而是@State装饰的变量。
71  ```ts
72  import { UIUtils } from '@kit.ArkUI';
73  class Person {
74    age: number = 10;
75  }
76  class Info {
77    id: number = 0;
78    person: Person = new Person();
79  }
80  @Entry
81  @Component
82  struct Index {
83    @State message: Info = new Info();
84    @State message2: Info = UIUtils.makeObserved(this.message); // 不会抛异常
85    build() {
86      Column() {
87        Text(`${this.message2.person.age}`)
88          .onClick(() => {
89            // UI不会刷新,因为State只能观察到第一层的变化
90            this.message2.person.age++;
91          })
92      }
93    }
94  }
95  ```
96### makeObserved仅对入参生效,不会改变接受返回值的观察能力
97
98 - `message`被@Local装饰,本身具有观察自身赋值的能力。其初始值为makeObserved的返回值,具有深度观察能力。
99 - 点击`change id`可以触发UI刷新。
100 - 点击`change Info`将`this.message`重新赋值为不可观察数据后,再次点击`change id`无法触发UI刷新。
101 - 再次点击`change Info1`将`this.message`重新赋值为可观察数据后,点击`change id`可以触发UI刷新。
102
103  ```ts
104  import { UIUtils } from '@kit.ArkUI';
105  class Info {
106    id: number = 0;
107    constructor(id: number) {
108      this.id = id;
109    }
110  }
111  @Entry
112  @ComponentV2
113  struct Index {
114    @Local message: Info = UIUtils.makeObserved(new Info(20));
115    build() {
116      Column() {
117        Button(`change id`).onClick(() => {
118          this.message.id++;
119        })
120        Button(`change Info ${this.message.id}`).onClick(() => {
121          this.message = new Info(30);
122        })
123        Button(`change Info1 ${this.message.id}`).onClick(() => {
124          this.message = UIUtils.makeObserved(new Info(30));
125        })
126      }
127    }
128  }
129  ```
130
131## 支持类型和观察变化
132
133### 支持类型
134
135- 支持未被@Observed或@ObserveV2装饰的类。
136- 支持Array、Map、Set和Date。
137- 支持collections.Array, collections.Setcollections.Map138- JSON.parse返回的Object。
139- @Sendable装饰的类。
140
141### 观察变化
142
143- makeObserved传入内置类型或collections类型的实例时,可以观测其API带来的变化:
144
145  | 类型  | 可观测变化的API                                              |
146  | ----- | ------------------------------------------------------------ |
147  | Array | push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort |
148  | collections.Array | push、pop、shift、unshift、splice、fill、reverse、sort、shrinkTo、extendTo |
149  | Map/collections.Map   | set、clear、delete                                 |
150  | Set/collections.Set   | add、clear、delete                                 |
151  | Date  | setFullYear、setMonth、setDate、setHours、setMinutes、setSeconds、setMilliseconds、setTime、setUTCFullYear、setUTCMonth、setUTCDate、setUTCHours、setUTCMinutes、setUTCSeconds、setUTCMilliseconds |
152
153## 使用场景
154
155### makeObserved和@Sendable装饰的class配合使用
156
157[@Sendable](../arkts-utils/arkts-sendable.md)主要是为了处理应用场景中的并发任务。将makeObserved和@Sendable配合使用是为了满足一般应用开发中,在子线程做大数据处理,在UI线程做ViewModel的显示和观察数据的需求。@Sendable具体内容可参考[并发任务文档](../arkts-utils/multi-thread-concurrency-overview.md)。
158
159本章节将说明下面的场景:
160- makeObserved在传入@Sendable类型的数据后有观察能力,且其变化可以触发UI刷新。
161- 从子线程中获取一个整体数据,然后对UI线程的可观察数据做整体替换。
162- 从子线程获取的数据重新执行makeObserved,将数据变为可观察数据。
163- 将数据从主线程传递回子线程时,仅传递不可观察的数据。makeObserved的返回值不可直接传给子线程。
164
165例子如下:
166
167```ts
168// SendableData.ets
169@Sendable
170export class SendableData  {
171  name: string = 'Tom';
172  age: number = 20;
173  gender: number = 1;
174  // ....更多其他属性
175  likes: number = 1;
176  follow: boolean = false;
177}
178```
179
180```ts
181import { taskpool } from '@kit.ArkTS';
182import { SendableData } from './SendableData';
183import { UIUtils } from '@kit.ArkUI';
184
185
186@Concurrent
187function threadGetData(param: string): SendableData {
188  // 在子线程处理数据
189  let ret = new SendableData();
190  console.info(`Concurrent threadGetData, param ${param}`);
191  ret.name = param + "-o";
192  ret.age = Math.floor(Math.random() * 40);
193  ret.likes = Math.floor(Math.random() * 100);
194  return ret;
195}
196
197@Entry
198@ComponentV2
199struct ObservedSendableTest {
200  // 通过makeObserved给普通对象或是Sendable对象添加可观察能力
201  @Local send: SendableData = UIUtils.makeObserved(new SendableData());
202  build() {
203    Column() {
204      Text(this.send.name)
205      Button("change name").onClick(() => {
206        // ok 可以观察到属性的改变
207        this.send.name += "0";
208      })
209
210      Button("task").onClick(() => {
211        // 将待执行的函数放入taskpool内部任务队列等待,等待分发到工作线程执行。
212        taskpool.execute(threadGetData, this.send.name).then(val => {
213          // 和@Local一起使用,可以观察this.send的变化
214          this.send = UIUtils.makeObserved(val as SendableData);
215        })
216      })
217    }
218  }
219}
220```
221需要注意:数据的构建和处理可以在子线程中完成,但有观察能力的数据不能传给子线程,只有在主线程里才可以操作可观察的数据。所以上述例子中只是将`this.send`的属性`name`传给子线程操作。
222
223### makeObserved和collections.Array/Set/Map配合使用
224collections提供ArkTS容器集,可用于并发场景下的高性能数据传递。详情见[@arkts.collections文档](../reference/apis-arkts/js-apis-arkts-collections.md)。
225makeObserved可以在ArkUI中导入可观察的colletions容器,但makeObserved不能和状态管理V1的状态变量装饰器如@State和@Prop等配合使用,否则会抛出运行时异常。
226
227#### collections.Array
228collections.Array可以触发UI刷新的API有:
229- 改变数组长度:push、pop、shift、unshift、splice、shrinkTo、extendTo
230- 改变数组项本身:sort、fill
231
232其他API不会改变原始数组,所以不会触发UI刷新。
233
234```ts
235import { collections } from '@kit.ArkTS';
236import { UIUtils } from '@kit.ArkUI';
237
238@Sendable
239class Info {
240  id: number = 0;
241  name: string = 'cc';
242
243  constructor(id: number) {
244    this.id = id;
245  }
246}
247
248
249@Entry
250@ComponentV2
251struct Index {
252  scroller: Scroller = new Scroller();
253  @Local arrCollect: collections.Array<Info> =
254    UIUtils.makeObserved(new collections.Array<Info>(new Info(1), new Info(2)));
255
256  build() {
257    Column() {
258      // ForEach接口仅支持Array<any>,不支持collections.Array<any>。
259      // 但ForEach的实现用到的Array的API,collections.Array都有提供。所以可以使用as类型断言Array。
260      // 需要注意断言并不会改变原本的数据类型。
261      ForEach(this.arrCollect as object as Array<Info>, (item: Info) => {
262        Text(`${item.id}`).onClick(() => {
263          item.id++;
264        })
265      }, (item: Info, index) => item.id.toString() + index.toString())
266      Divider()
267        .color('blue')
268      if (this.arrCollect.length > 0) {
269        Text(`the first one ${this.arrCollect[this.arrCollect.length - this.arrCollect.length].id}`)
270        Text(`the last one ${this.arrCollect[this.arrCollect.length - 1].id}`)
271      }
272      Divider()
273        .color('blue')
274
275      /****************************改变数据长度的api**************************/
276      Scroll(this.scroller) {
277        Column({space: 10}) {
278          // push: 新增新元素
279          Button('push').onClick(() => {
280            this.arrCollect.push(new Info(30));
281          })
282          // pop: 删除最后一个
283          Button('pop').onClick(() => {
284            this.arrCollect.pop();
285          })
286          // shift: 删除第一个
287          Button('shift').onClick(() => {
288            this.arrCollect.shift();
289          })
290          // unshift: 在数组的开头插入新项
291          Button('unshift').onClick(() => {
292            this.arrCollect.unshift(new Info(50));
293          })
294          // splice: 从数组的指定位置删除元素
295          Button('splice').onClick(() => {
296            this.arrCollect.splice(1);
297          })
298
299          // shrinkTo: 将数组长度缩小到给定的长度
300          Button('shrinkTo').onClick(() => {
301            this.arrCollect.shrinkTo(1);
302          })
303          // extendTo: 将数组长度扩展到给定的长度
304          Button('extendTo').onClick(() => {
305            this.arrCollect.extendTo(6, new Info(20));
306          })
307
308          Divider()
309            .color('blue')
310
311          /****************************************改变数组item本身*****************/
312          // sort:从大到小排序
313          Button('sort').onClick(() => {
314            this.arrCollect.sort((a: Info, b: Info) => b.id - a.id);
315          })
316          // fill: 用值填充指定部分
317          Button('fill').onClick(() => {
318            this.arrCollect.fill(new Info(5), 0, 2);
319          })
320
321          /*****************************不会改变数组本身API***************************/
322          // slice:返回新的数组,根据start end对原数组的拷贝,不会改变原数组,所以直接调用slice不会触发UI刷新
323          // 可以构建用例为返回的浅拷贝的数据赋值给this.arrCollect,需要注意这里依然要调用makeObserved,否则this.arr被普通变量赋值后,会丧失观察能力
324          Button('slice').onClick(() => {
325            this.arrCollect = UIUtils.makeObserved(this.arrCollect.slice(0, 1));
326          })
327          // map:原理同上
328          Button('map').onClick(() => {
329            this.arrCollect = UIUtils.makeObserved(this.arrCollect.map((value) => {
330              value.id += 10;
331              return value;
332            }))
333          })
334          // filter:原理同上
335          Button('filter').onClick(() => {
336            this.arrCollect = UIUtils.makeObserved(this.arrCollect.filter((value: Info) => value.id % 2 === 0));
337          })
338
339          // concat:原理同上
340          Button('concat').onClick(() => {
341            let array1 = new collections.Array(new Info(100))
342            this.arrCollect = UIUtils.makeObserved(this.arrCollect.concat(array1));
343          })
344        }.height('200%')
345      }.height('60%')
346    }
347    .height('100%')
348    .width('100%')
349  }
350}
351```
352#### collections.Map
353
354collections.Map可以触发UI刷新的API有:set、clear、delete。
355```ts
356import { collections } from '@kit.ArkTS';
357import { UIUtils } from '@kit.ArkUI';
358
359@Sendable
360class Info {
361  id: number = 0;
362
363  constructor(id: number) {
364    this.id = id;
365  }
366}
367
368
369@Entry
370@ComponentV2
371struct CollectionMap {
372  mapCollect: collections.Map<string, Info> = UIUtils.makeObserved(new collections.Map<string, Info>([['a', new Info(10)], ['b', new Info(20)]]));
373
374  build() {
375    Column() {
376      // this.mapCollect.keys()返回迭代器。Foreach不支持迭代器,所以要Array.From浅拷贝生成数据。
377      ForEach(Array.from(this.mapCollect.keys()), (item: string) => {
378        Text(`${this.mapCollect.get(item)?.id}`).onClick(() => {
379          let value: Info|undefined = this.mapCollect.get(item);
380          if (value) {
381            value.id++;
382          }
383        })
384      }, (item: string, index) => item + index.toString())
385
386      // set c
387      Button('set c').onClick(() => {
388        this.mapCollect.set('c', new Info(30));
389      })
390      // delete c
391      Button('delete c').onClick(() => {
392        if (this.mapCollect.has('c')) {
393          this.mapCollect.delete('c');
394        }
395      })
396      // clear
397      Button('clear').onClick(() => {
398        this.mapCollect.clear();
399      })
400    }
401    .height('100%')
402    .width('100%')
403  }
404}
405```
406
407#### collections.Set
408collections.Set可以触发UI刷新的API有:add、clear、delete。
409
410```ts
411import { collections } from '@kit.ArkTS';
412import { UIUtils } from '@kit.ArkUI';
413@Sendable
414class Info {
415  id: number = 0;
416
417  constructor(id: number) {
418    this.id = id;
419  }
420}
421
422
423@Entry
424@ComponentV2
425struct Index {
426  set: collections.Set<Info> = UIUtils.makeObserved(new collections.Set<Info>([new Info(10), new Info(20)]));
427
428  build() {
429    Column() {
430      // 因为ForEach不支持迭代器,所以需要使用Array.from浅拷贝生成数组。
431      // 但是浅拷贝生成的新的数组没有观察能力,为了ForEach组件在访问item的时候是可观察的数据,所以需要重新调用makeObserved。
432      ForEach((UIUtils.makeObserved(Array.from(this.set.values()))), (item: Info) => {
433        Text(`${item.id}`).onClick(() => {
434          item.id++;
435        })
436      }, (item: Info, index) => item.id + index.toString())
437
438      // add
439      Button('add').onClick(() => {
440        this.set.add(new Info(30));
441        console.log('size:' + this.set.size);
442      })
443      // delete
444      Button('delete').onClick(() => {
445        let iterator = this.set.keys();
446        this.set.delete(iterator.next().value);
447      })
448      // clear
449      Button('clear').onClick(() => {
450        this.set.clear();
451      })
452    }
453    .height('100%')
454    .width('100%')
455  }
456}
457```
458
459### makeObserved的入参为JSON.parse的返回值
460JSON.parse返回Object,无法使用@Trace装饰其属性,可以使用makeObserved使其变为可观察数据。
461
462```ts
463import { JSON } from '@kit.ArkTS';
464import { UIUtils } from '@kit.ArkUI';
465
466class Info {
467  id: number = 0;
468
469  constructor(id: number) {
470    this.id = id;
471  }
472}
473
474let test: Record<string, number> = { "a": 123 };
475let testJsonStr :string = JSON.stringify(test);
476let test2: Record<string, Info> = { "a": new Info(20) };
477let test2JsonStr: string = JSON.stringify(test2);
478
479@Entry
480@ComponentV2
481struct Index {
482  message: Record<string, number> = UIUtils.makeObserved<Record<string, number>>(JSON.parse(testJsonStr) as Record<string, number>);
483  message2: Record<string, Info> = UIUtils.makeObserved<Record<string, Info>>(JSON.parse(test2JsonStr) as Record<string, Info>);
484
485  build() {
486    Column() {
487      Text(`${this.message.a}`)
488        .fontSize(50)
489        .onClick(() => {
490          this.message.a++;
491        })
492      Text(`${this.message2.a.id}`)
493        .fontSize(50)
494        .onClick(() => {
495          this.message2.a.id++;
496        })
497    }
498    .height('100%')
499    .width('100%')
500  }
501}
502```
503
504### makeObserved和V2装饰器配合使用
505makeObserved可以和V2的装饰器一起使用。对于@Monitor和@Computed,因为makeObserved传入@Observed或ObservedV2装饰的类实例会返回其自身,所以@Monitor或者@Computed不能定义在class中,只能定义在自定义组件里。
506
507例子如下:
508```ts
509import { UIUtils } from '@kit.ArkUI';
510
511class Info {
512  id: number = 0;
513  age: number = 20;
514
515  constructor(id: number) {
516    this.id = id;
517  }
518}
519
520@Entry
521@ComponentV2
522struct Index {
523  @Local message: Info = UIUtils.makeObserved(new Info(20));
524
525  @Monitor('message.id')
526  onStrChange(monitor: IMonitor) {
527    console.log(`name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
528  }
529
530  @Computed
531  get ageId() {
532    console.info("---------Computed----------");
533    return this.message.id + ' ' + this.message.age;
534  }
535
536  build() {
537    Column() {
538      Text(`id: ${this.message.id}`)
539        .fontSize(50)
540        .onClick(() => {
541          this.message.id++;
542        })
543
544      Text(`age: ${this.message.age}`)
545        .fontSize(50)
546        .onClick(() => {
547          this.message.age++;
548        })
549
550      Text(`Computed age+id: ${this.ageId}`)
551        .fontSize(50)
552
553      Button('change Info').onClick(() => {
554        this.message = UIUtils.makeObserved(new Info(200));
555      })
556
557      Child({message: this.message})
558    }
559    .height('100%')
560    .width('100%')
561  }
562}
563
564@ComponentV2
565struct Child {
566  @Param @Require message: Info;
567  build() {
568    Text(`Child id: ${this.message.id}`)
569  }
570}
571```
572
573### makeObserved在@Component内使用
574makeObserved不能和V1的状态变量装饰器一起使用,但可以在@Component装饰的自定义组件里使用。
575
576```ts
577import { UIUtils } from '@kit.ArkUI';
578class Info {
579  id: number = 0;
580
581  constructor(id: number) {
582    this.id = id;
583  }
584}
585
586
587@Entry
588@Component
589struct Index {
590  // 如果和@State一起使用会抛出运行时异常
591  message: Info = UIUtils.makeObserved(new Info(20));
592
593  build() {
594    RelativeContainer() {
595      Text(`${this.message.id}`)
596        .onClick(() => {
597          this.message.id++;
598        })
599    }
600    .height('100%')
601    .width('100%')
602  }
603}
604```
605
606## 常见问题
607### getTarget后的数据可以正常赋值,但是无法触发UI刷新
608[getTarget](./arkts-new-getTarget.md)可以获取状态管理框架代理前的原始对象。
609
610makeObserved封装的观察对象,可以通过getTarget获取到其原始对象,对原始对象的赋值不会触发UI刷新。
611
612如下面例子:
6131. 先点击第一个Text组件,通过getTarget获取其原始对象,此时修改原始对象的属性不会触发UI刷新,但数据会正常赋值。
6142. 再点击第二个Text组件,此时修改`this.observedObj`的属性会触发UI刷新,Text显示21。
615
616```ts
617import { UIUtils } from '@kit.ArkUI';
618class Info {
619  id: number = 0;
620}
621
622@Entry
623@Component
624struct Index {
625  observedObj: Info = UIUtils.makeObserved(new Info());
626  build() {
627    Column() {
628      Text(`${this.observedObj.id}`)
629        .fontSize(50)
630        .onClick(() => {
631          // 通过getTarget获取其原始对象,将this.observedObj赋值为不可观察的数据
632          let rawObj: Info= UIUtils.getTarget(this.observedObj);
633          // 不会触发UI刷新,但数据会正常赋值
634          rawObj.id = 20;
635        })
636
637      Text(`${this.observedObj.id}`)
638        .fontSize(50)
639        .onClick(() => {
640          // 触发UI刷新,Text显示21
641          this.observedObj.id++;
642        })
643    }
644  }
645}
646```