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