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