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.Set和collections.Map。 144- 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```