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