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