1# 状态管理合理使用开发指导 2 3由于对状态管理当前的特性并不了解,许多开发者在使用状态管理进行开发时会遇到UI不刷新、刷新性能差的情况。对此,本篇将从两个方向,对一共五个典型场景进行分析,同时提供相应的正例和反例,帮助开发者学习如何合理使用状态管理进行开发。 4 5## 合理使用属性 6 7### 将简单属性数组合并成对象数组 8 9在开发过程中,我们经常会需要设置多个组件的同一种属性,比如Text组件的内容、组件的宽度、高度等样式信息等。将这些属性保存在一个数组中,配合ForEach进行使用是一种简单且方便的方法。 10 11```typescript 12@Entry 13@Component 14struct Index { 15 @State items: string[] = []; 16 @State ids: string[] = []; 17 @State age: number[] = []; 18 @State gender: string[] = []; 19 20 aboutToAppear() { 21 this.items.push("Head"); 22 this.items.push("List"); 23 for (let i = 0; i < 20; i++) { 24 this.ids.push("id: " + Math.floor(Math.random() * 1000)); 25 this.age.push(Math.floor(Math.random() * 100 % 40)); 26 this.gender.push(Math.floor(Math.random() * 100) % 2 == 0 ? "Male" : "Female"); 27 } 28 } 29 30 isRenderText(index: number) : number { 31 console.log(`index ${index} is rendered`); 32 return 1; 33 } 34 35 build() { 36 Row() { 37 Column() { 38 ForEach(this.items, (item: string) => { 39 if (item == "Head") { 40 Text("Personal Info") 41 .fontSize(40) 42 } else if (item == "List") { 43 List() { 44 ForEach(this.ids, (id: string, index) => { 45 ListItem() { 46 Row() { 47 Text(id) 48 .fontSize(20) 49 .margin({ 50 left: 30, 51 right: 5 52 }) 53 Text("age: " + this.age[index as number]) 54 .fontSize(20) 55 .margin({ 56 left: 5, 57 right: 5 58 }) 59 .position({x: 100}) 60 .opacity(this.isRenderText(index)) 61 .onClick(() => { 62 this.age[index]++; 63 }) 64 Text("gender: " + this.gender[index as number]) 65 .margin({ 66 left: 5, 67 right: 5 68 }) 69 .position({x: 180}) 70 .fontSize(20) 71 } 72 } 73 .margin({ 74 top: 5, 75 bottom: 5 76 }) 77 }) 78 } 79 } 80 }) 81 } 82 } 83 } 84} 85``` 86 87上述代码运行效果如下。 88 89 90 91页面内通过ForEach显示了20条信息,当点击某一条信息中age的Text组件时,可以通过日志发现其他的19条信息中age的Text组件也进行了刷新(这体现在日志上,所有的age的Text组件都打出了日志),但实际上其他19条信息的age的数值并没有改变,也就是说其他19个Text组件并不需要刷新。 92 93这是因为当前状态管理的一个特性。假设存在一个被@State修饰的number类型的数组Num[],其中有20个元素,值分别为0到19。这20个元素分别绑定了一个Text组件,当改变其中一个元素,例如第0号元素的值从0改成1,除了0号元素绑定的Text组件会刷新之外,其他的19个Text组件也会刷新,即使1到19号元素的值并没有改变。 94 95这个特性普遍的出现在简单类型数组的场景中,当数组中的元素够多时,会对UI的刷新性能有很大的负面影响。这种“不需要刷新的组件被刷新”的现象即是“冗余刷新”,当“冗余刷新”的节点过多时,UI的刷新效率会大幅度降低,因此需要减少“冗余刷新”,也就是做到**精准控制组件的更新范围**。 96 97为了减少由简单的属性相关的数组引起的“冗余刷新”,需要将属性数组转变为对象数组,配合自定义组件,实现精准控制更新范围。下面为修改后的代码。 98 99```typescript 100@Observed 101class InfoList extends Array<Info> { 102}; 103@Observed 104class Info { 105 ids: number; 106 age: number; 107 gender: string; 108 109 constructor() { 110 this.ids = Math.floor(Math.random() * 1000); 111 this.age = Math.floor(Math.random() * 100 % 40); 112 this.gender = Math.floor(Math.random() * 100) % 2 == 0 ? "Male" : "Female"; 113 } 114} 115@Component 116struct Information { 117 @ObjectLink info: Info; 118 @State index: number = 0; 119 isRenderText(index: number) : number { 120 console.log(`index ${index} is rendered`); 121 return 1; 122 } 123 124 build() { 125 Row() { 126 Text("id: " + this.info.ids) 127 .fontSize(20) 128 .margin({ 129 left: 30, 130 right: 5 131 }) 132 Text("age: " + this.info.age) 133 .fontSize(20) 134 .margin({ 135 left: 5, 136 right: 5 137 }) 138 .position({x: 100}) 139 .opacity(this.isRenderText(this.index)) 140 .onClick(() => { 141 this.info.age++; 142 }) 143 Text("gender: " + this.info.gender) 144 .margin({ 145 left: 5, 146 right: 5 147 }) 148 .position({x: 180}) 149 .fontSize(20) 150 } 151 } 152} 153@Entry 154@Component 155struct Page { 156 @State infoList: InfoList = new InfoList(); 157 @State items: string[] = []; 158 aboutToAppear() { 159 this.items.push("Head"); 160 this.items.push("List"); 161 for (let i = 0; i < 20; i++) { 162 this.infoList.push(new Info()); 163 } 164 } 165 166 build() { 167 Row() { 168 Column() { 169 ForEach(this.items, (item: string) => { 170 if (item == "Head") { 171 Text("Personal Info") 172 .fontSize(40) 173 } else if (item == "List") { 174 List() { 175 ForEach(this.infoList, (info: Info, index) => { 176 ListItem() { 177 Information({ 178 // in low version, DevEco may throw a warning, but it does not matter. 179 // you can still compile and run. 180 info: info, 181 index: index 182 }) 183 } 184 .margin({ 185 top: 5, 186 bottom: 5 187 }) 188 }) 189 } 190 } 191 }) 192 } 193 } 194 } 195} 196``` 197 198上述代码的运行效果如下。 199 200 201 202修改后的代码使用对象数组代替了原有的多个属性数组,能够避免数组的“冗余刷新”的情况。这是因为对于数组来说,对象内的变化是无法感知的,数组只能观测数组项层级的变化,例如新增数据项,修改数据项(普通数组是直接修改数据项的值,在对象数组的场景下是整个对象被重新赋值,改变某个数据项对象中的属性不会被观测到)、删除数据项等。这意味着当改变对象内的某个属性时,对于数组来说,对象是没有变化的,也就不会去刷新。在当前状态管理的观测能力中,除了数组嵌套对象的场景外,对象嵌套对象的场景也是无法观测到变化的,这一部分内容将在[将复杂对象拆分成多个小对象的集合](#将复杂大对象拆分成多个小对象的集合)中讲到。同时修改代码时使用了自定义组件与ForEach的结合,这一部分内容将在[在ForEach中使用自定义组件搭配对象数组](#在foreach中使用自定义组件搭配对象数组)讲到。 203 204### 将复杂大对象拆分成多个小对象的集合 205 206> **说明:** 207> 208> 从API version 11开始,推荐优先使用[@Track装饰器](arkts-track.md)解决该场景的问题。 209 210在开发过程中,我们有时会定义一个大的对象,其中包含了很多样式相关的属性,并且在父子组件间传递这个对象,将其中的属性绑定在组件上。 211 212```typescript 213@Observed 214class UIStyle { 215 translateX: number = 0; 216 translateY: number = 0; 217 scaleX: number = 0.3; 218 scaleY: number = 0.3; 219 width: number = 336; 220 height: number = 178; 221 posX: number = 10; 222 posY: number = 50; 223 alpha: number = 0.5; 224 borderRadius: number = 24; 225 imageWidth: number = 78; 226 imageHeight: number = 78; 227 translateImageX: number = 0; 228 translateImageY: number = 0; 229 fontSize: number = 20; 230} 231@Component 232struct SpecialImage { 233 @ObjectLink uiStyle: UIStyle; 234 private isRenderSpecialImage() : number { // function to show whether the component is rendered 235 console.log("SpecialImage is rendered"); 236 return 1; 237 } 238 build() { 239 Image($r('app.media.icon')) // 在API12及以后的工程中使用app.media.app_icon 240 .width(this.uiStyle.imageWidth) 241 .height(this.uiStyle.imageHeight) 242 .margin({ top: 20 }) 243 .translate({ 244 x: this.uiStyle.translateImageX, 245 y: this.uiStyle.translateImageY 246 }) 247 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 248 } 249} 250@Component 251struct CompA { 252 @ObjectLink uiStyle: UIStyle 253 // the following functions are used to show whether the component is called to be rendered 254 private isRenderColumn() : number { 255 console.log("Column is rendered"); 256 return 1; 257 } 258 private isRenderStack() : number { 259 console.log("Stack is rendered"); 260 return 1; 261 } 262 private isRenderImage() : number { 263 console.log("Image is rendered"); 264 return 1; 265 } 266 private isRenderText() : number { 267 console.log("Text is rendered"); 268 return 1; 269 } 270 build() { 271 Column() { 272 SpecialImage({ 273 // in low version, Dev Eco may throw a warning 274 // But you can still build and run the code 275 uiStyle: this.uiStyle 276 }) 277 Stack() { 278 Column() { 279 Image($r('app.media.icon')) // 在API12及以后的工程中使用app.media.app_icon 280 .opacity(this.uiStyle.alpha) 281 .scale({ 282 x: this.uiStyle.scaleX, 283 y: this.uiStyle.scaleY 284 }) 285 .padding(this.isRenderImage()) 286 .width(300) 287 .height(300) 288 } 289 .width('100%') 290 .position({ y: -80 }) 291 Stack() { 292 Text("Hello World") 293 .fontColor("#182431") 294 .fontWeight(FontWeight.Medium) 295 .fontSize(this.uiStyle.fontSize) 296 .opacity(this.isRenderText()) 297 .margin({ top: 12 }) 298 } 299 .opacity(this.isRenderStack()) 300 .position({ 301 x: this.uiStyle.posX, 302 y: this.uiStyle.posY 303 }) 304 .width('100%') 305 .height('100%') 306 } 307 .margin({ top: 50 }) 308 .borderRadius(this.uiStyle.borderRadius) 309 .opacity(this.isRenderStack()) 310 .backgroundColor("#FFFFFF") 311 .width(this.uiStyle.width) 312 .height(this.uiStyle.height) 313 .translate({ 314 x: this.uiStyle.translateX, 315 y: this.uiStyle.translateY 316 }) 317 Column() { 318 Button("Move") 319 .width(312) 320 .fontSize(20) 321 .backgroundColor("#FF007DFF") 322 .margin({ bottom: 10 }) 323 .onClick(() => { 324 animateTo({ 325 duration: 500 326 },() => { 327 this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250; 328 }) 329 }) 330 Button("Scale") 331 .borderRadius(20) 332 .backgroundColor("#FF007DFF") 333 .fontSize(20) 334 .width(312) 335 .onClick(() => { 336 this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8; 337 }) 338 } 339 .position({ 340 y:666 341 }) 342 .height('100%') 343 .width('100%') 344 345 } 346 .opacity(this.isRenderColumn()) 347 .width('100%') 348 .height('100%') 349 350 } 351} 352@Entry 353@Component 354struct Page { 355 @State uiStyle: UIStyle = new UIStyle(); 356 build() { 357 Stack() { 358 CompA({ 359 // in low version, Dev Eco may throw a warning 360 // But you can still build and run the code 361 uiStyle: this.uiStyle 362 }) 363 } 364 .backgroundColor("#F1F3F5") 365 } 366} 367``` 368 369上述代码的运行效果如下。 370 371 372 373优化前点击move按钮的脏节点更新耗时如下图: 374 375 376 377在上面的示例中,UIStyle定义了多个属性,并且这些属性分别被多个组件关联。当点击任意一个按钮更改其中的某些属性时,会导致所有这些关联uiStyle的组件进行刷新,虽然它们其实并不需要进行刷新(因为组件的属性都没有改变)。通过定义的一系列isRender函数,可以观察到这些组件的刷新。当点击“move”按钮进行平移动画时,由于translateY的值的多次改变,会导致每一次都存在“冗余刷新”的问题,这对应用的性能有着很大的负面影响。 378 379这是因为当前状态管理的一个刷新机制,假设定义了一个有20个属性的类,创建类的对象实例,将20个属性绑定到组件上,这时修改其中的某个属性,除了这个属性关联的组件会刷新之外,其他的19个属性关联的组件也都会刷新,即使这些属性本身并没有发生变化。 380 381这个机制会导致在使用一个复杂大对象与多个组件关联时,刷新性能的下降。对此,推荐将一个复杂大对象拆分成多个小对象的集合,在保留原有代码结构的基础上,减少“冗余刷新”,实现精准控制组件的更新范围。 382 383```typescript 384@Observed 385class NeedRenderImage { // properties only used in the same component can be divided into the same new divided class 386 public translateImageX: number = 0; 387 public translateImageY: number = 0; 388 public imageWidth:number = 78; 389 public imageHeight:number = 78; 390} 391@Observed 392class NeedRenderScale { // properties usually used together can be divided into the same new divided class 393 public scaleX: number = 0.3; 394 public scaleY: number = 0.3; 395} 396@Observed 397class NeedRenderAlpha { // properties that may be used in different places can be divided into the same new divided class 398 public alpha: number = 0.5; 399} 400@Observed 401class NeedRenderSize { // properties usually used together can be divided into the same new divided class 402 public width: number = 336; 403 public height: number = 178; 404} 405@Observed 406class NeedRenderPos { // properties usually used together can be divided into the same new divided class 407 public posX: number = 10; 408 public posY: number = 50; 409} 410@Observed 411class NeedRenderBorderRadius { // properties that may be used in different places can be divided into the same new divided class 412 public borderRadius: number = 24; 413} 414@Observed 415class NeedRenderFontSize { // properties that may be used in different places can be divided into the same new divided class 416 public fontSize: number = 20; 417} 418@Observed 419class NeedRenderTranslate { // properties usually used together can be divided into the same new divided class 420 public translateX: number = 0; 421 public translateY: number = 0; 422} 423@Observed 424class UIStyle { 425 // define new variable instead of using old one 426 needRenderTranslate: NeedRenderTranslate = new NeedRenderTranslate(); 427 needRenderFontSize: NeedRenderFontSize = new NeedRenderFontSize(); 428 needRenderBorderRadius: NeedRenderBorderRadius = new NeedRenderBorderRadius(); 429 needRenderPos: NeedRenderPos = new NeedRenderPos(); 430 needRenderSize: NeedRenderSize = new NeedRenderSize(); 431 needRenderAlpha: NeedRenderAlpha = new NeedRenderAlpha(); 432 needRenderScale: NeedRenderScale = new NeedRenderScale(); 433 needRenderImage: NeedRenderImage = new NeedRenderImage(); 434} 435@Component 436struct SpecialImage { 437 @ObjectLink uiStyle : UIStyle; 438 @ObjectLink needRenderImage: NeedRenderImage // receive the new class from its parent component 439 private isRenderSpecialImage() : number { // function to show whether the component is rendered 440 console.log("SpecialImage is rendered"); 441 return 1; 442 } 443 build() { 444 Image($r('app.media.icon')) // 在API12及以后的工程中使用app.media.app_icon 445 .width(this.needRenderImage.imageWidth) // !! use this.needRenderImage.xxx rather than this.uiStyle.needRenderImage.xxx !! 446 .height(this.needRenderImage.imageHeight) 447 .margin({top:20}) 448 .translate({ 449 x: this.needRenderImage.translateImageX, 450 y: this.needRenderImage.translateImageY 451 }) 452 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 453 } 454} 455@Component 456struct CompA { 457 @ObjectLink uiStyle: UIStyle; 458 @ObjectLink needRenderTranslate: NeedRenderTranslate; // receive the new class from its parent component 459 @ObjectLink needRenderFontSize: NeedRenderFontSize; 460 @ObjectLink needRenderBorderRadius: NeedRenderBorderRadius; 461 @ObjectLink needRenderPos: NeedRenderPos; 462 @ObjectLink needRenderSize: NeedRenderSize; 463 @ObjectLink needRenderAlpha: NeedRenderAlpha; 464 @ObjectLink needRenderScale: NeedRenderScale; 465 // the following functions are used to show whether the component is called to be rendered 466 private isRenderColumn() : number { 467 console.log("Column is rendered"); 468 return 1; 469 } 470 private isRenderStack() : number { 471 console.log("Stack is rendered"); 472 return 1; 473 } 474 private isRenderImage() : number { 475 console.log("Image is rendered"); 476 return 1; 477 } 478 private isRenderText() : number { 479 console.log("Text is rendered"); 480 return 1; 481 } 482 build() { 483 Column() { 484 SpecialImage({ 485 // in low version, Dev Eco may throw a warning 486 // But you can still build and run the code 487 uiStyle: this.uiStyle, 488 needRenderImage: this.uiStyle.needRenderImage //send it to its child 489 }) 490 Stack() { 491 Column() { 492 Image($r('app.media.icon')) // 在API12及以后的工程中使用app.media.app_icon 493 .opacity(this.needRenderAlpha.alpha) 494 .scale({ 495 x: this.needRenderScale.scaleX, // use this.needRenderXxx.xxx rather than this.uiStyle.needRenderXxx.xxx 496 y: this.needRenderScale.scaleY 497 }) 498 .padding(this.isRenderImage()) 499 .width(300) 500 .height(300) 501 } 502 .width('100%') 503 .position({ y: -80 }) 504 505 Stack() { 506 Text("Hello World") 507 .fontColor("#182431") 508 .fontWeight(FontWeight.Medium) 509 .fontSize(this.needRenderFontSize.fontSize) 510 .opacity(this.isRenderText()) 511 .margin({ top: 12 }) 512 } 513 .opacity(this.isRenderStack()) 514 .position({ 515 x: this.needRenderPos.posX, 516 y: this.needRenderPos.posY 517 }) 518 .width('100%') 519 .height('100%') 520 } 521 .margin({ top: 50 }) 522 .borderRadius(this.needRenderBorderRadius.borderRadius) 523 .opacity(this.isRenderStack()) 524 .backgroundColor("#FFFFFF") 525 .width(this.needRenderSize.width) 526 .height(this.needRenderSize.height) 527 .translate({ 528 x: this.needRenderTranslate.translateX, 529 y: this.needRenderTranslate.translateY 530 }) 531 532 Column() { 533 Button("Move") 534 .width(312) 535 .fontSize(20) 536 .backgroundColor("#FF007DFF") 537 .margin({ bottom: 10 }) 538 .onClick(() => { 539 animateTo({ 540 duration: 500 541 }, () => { 542 this.needRenderTranslate.translateY = (this.needRenderTranslate.translateY + 180) % 250; 543 }) 544 }) 545 Button("Scale") 546 .borderRadius(20) 547 .backgroundColor("#FF007DFF") 548 .fontSize(20) 549 .width(312) 550 .margin({ bottom: 10 }) 551 .onClick(() => { 552 this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 0.8; 553 }) 554 Button("Change Image") 555 .borderRadius(20) 556 .backgroundColor("#FF007DFF") 557 .fontSize(20) 558 .width(312) 559 .onClick(() => { // in the parent component, still use this.uiStyle.needRenderXxx.xxx to change the properties 560 this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 30) % 160; 561 this.uiStyle.needRenderImage.imageHeight = (this.uiStyle.needRenderImage.imageHeight + 30) % 160; 562 }) 563 } 564 .position({ 565 y: 616 566 }) 567 .height('100%') 568 .width('100%') 569 } 570 .opacity(this.isRenderColumn()) 571 .width('100%') 572 .height('100%') 573 } 574} 575@Entry 576@Component 577struct Page { 578 @State uiStyle: UIStyle = new UIStyle(); 579 build() { 580 Stack() { 581 CompA({ 582 // in low version, Dev Eco may throw a warning 583 // But you can still build and run the code 584 uiStyle: this.uiStyle, 585 needRenderTranslate: this.uiStyle.needRenderTranslate, //send all the new class child need 586 needRenderFontSize: this.uiStyle.needRenderFontSize, 587 needRenderBorderRadius: this.uiStyle.needRenderBorderRadius, 588 needRenderPos: this.uiStyle.needRenderPos, 589 needRenderSize: this.uiStyle.needRenderSize, 590 needRenderAlpha: this.uiStyle.needRenderAlpha, 591 needRenderScale: this.uiStyle.needRenderScale 592 }) 593 } 594 .backgroundColor("#F1F3F5") 595 } 596} 597``` 598 599上述代码的运行效果如下。 600 601优化后点击move按钮的脏节点更新耗时如下图: 602 603 604 605修改后的代码将原来的大类中的十五个属性拆成了八个小类,并且在绑定的组件上也做了相应的适配。属性拆分遵循以下几点原则: 606 607- 只作用在同一个组件上的多个属性可以被拆分进同一个新类,即示例中的NeedRenderImage。适用于组件经常被不关联的属性改变而引起刷新的场景,这个时候就要考虑拆分属性,或者重新考虑ViewModel设计是否合理。 608- 经常被同时使用的属性可以被拆分进同一个新类,即示例中的NeedRenderScale、NeedRenderTranslate、NeedRenderPos、NeedRenderSize。适用于属性经常成对出现,或者被作用在同一个样式上的情况,例如.translate、.position、.scale等(这些样式通常会接收一个对象作为参数)。 609- 可能被用在多个组件上或相对较独立的属性应该被单独拆分进一个新类,即示例中的NeedRenderAlpha,NeedRenderBorderRadius、NeedRenderFontSize。适用于一个属性作用在多个组件上或者与其他属性没有联系的情况,例如.opacity、.borderRadius等(这些样式通常相对独立)。 610 611属性拆分的原理和属性合并类似,都是在嵌套场景下,状态管理无法观测二层以上的属性变化,所以不会因为二层的数据变化导致一层关联的其他属性被刷新,同时利用@Observed和@ObjectLink在父子节点间传递二层的对象,从而在子组件中正常的观测二层的数据变化,实现精准刷新。<!--Del-->关于属性拆分的详细内容,可以查看[精准控制组件的更新范围](../performance/precisely-control-render-scope.md)。<!--DelEnd--> 612 613使用@Track装饰器则无需做属性拆分,也能达到同样控制组件更新范围的作用。 614 615```ts 616@Observed 617class UIStyle { 618 @Track translateX: number = 0; 619 @Track translateY: number = 0; 620 @Track scaleX: number = 0.3; 621 @Track scaleY: number = 0.3; 622 @Track width: number = 336; 623 @Track height: number = 178; 624 @Track posX: number = 10; 625 @Track posY: number = 50; 626 @Track alpha: number = 0.5; 627 @Track borderRadius: number = 24; 628 @Track imageWidth: number = 78; 629 @Track imageHeight: number = 78; 630 @Track translateImageX: number = 0; 631 @Track translateImageY: number = 0; 632 @Track fontSize: number = 20; 633} 634@Component 635struct SpecialImage { 636 @ObjectLink uiStyle: UIStyle; 637 private isRenderSpecialImage() : number { // function to show whether the component is rendered 638 console.log("SpecialImage is rendered"); 639 return 1; 640 } 641 build() { 642 Image($r('app.media.icon')) // 在API12及以后的工程中使用app.media.app_icon 643 .width(this.uiStyle.imageWidth) 644 .height(this.uiStyle.imageHeight) 645 .margin({ top: 20 }) 646 .translate({ 647 x: this.uiStyle.translateImageX, 648 y: this.uiStyle.translateImageY 649 }) 650 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 651 } 652} 653@Component 654struct CompA { 655 @ObjectLink uiStyle: UIStyle 656 // the following functions are used to show whether the component is called to be rendered 657 private isRenderColumn() : number { 658 console.log("Column is rendered"); 659 return 1; 660 } 661 private isRenderStack() : number { 662 console.log("Stack is rendered"); 663 return 1; 664 } 665 private isRenderImage() : number { 666 console.log("Image is rendered"); 667 return 1; 668 } 669 private isRenderText() : number { 670 console.log("Text is rendered"); 671 return 1; 672 } 673 build() { 674 Column() { 675 SpecialImage({ 676 // in low version, Dev Eco may throw a warning 677 // But you can still build and run the code 678 uiStyle: this.uiStyle 679 }) 680 Stack() { 681 Column() { 682 Image($r('app.media.icon')) // 在API12及以后的工程中使用app.media.app_icon 683 .opacity(this.uiStyle.alpha) 684 .scale({ 685 x: this.uiStyle.scaleX, 686 y: this.uiStyle.scaleY 687 }) 688 .padding(this.isRenderImage()) 689 .width(300) 690 .height(300) 691 } 692 .width('100%') 693 .position({ y: -80 }) 694 Stack() { 695 Text("Hello World") 696 .fontColor("#182431") 697 .fontWeight(FontWeight.Medium) 698 .fontSize(this.uiStyle.fontSize) 699 .opacity(this.isRenderText()) 700 .margin({ top: 12 }) 701 } 702 .opacity(this.isRenderStack()) 703 .position({ 704 x: this.uiStyle.posX, 705 y: this.uiStyle.posY 706 }) 707 .width('100%') 708 .height('100%') 709 } 710 .margin({ top: 50 }) 711 .borderRadius(this.uiStyle.borderRadius) 712 .opacity(this.isRenderStack()) 713 .backgroundColor("#FFFFFF") 714 .width(this.uiStyle.width) 715 .height(this.uiStyle.height) 716 .translate({ 717 x: this.uiStyle.translateX, 718 y: this.uiStyle.translateY 719 }) 720 Column() { 721 Button("Move") 722 .width(312) 723 .fontSize(20) 724 .backgroundColor("#FF007DFF") 725 .margin({ bottom: 10 }) 726 .onClick(() => { 727 animateTo({ 728 duration: 500 729 },() => { 730 this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250; 731 }) 732 }) 733 Button("Scale") 734 .borderRadius(20) 735 .backgroundColor("#FF007DFF") 736 .fontSize(20) 737 .width(312) 738 .onClick(() => { 739 this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8; 740 }) 741 } 742 .position({ 743 y:666 744 }) 745 .height('100%') 746 .width('100%') 747 748 } 749 .opacity(this.isRenderColumn()) 750 .width('100%') 751 .height('100%') 752 753 } 754} 755@Entry 756@Component 757struct Page { 758 @State uiStyle: UIStyle = new UIStyle(); 759 build() { 760 Stack() { 761 CompA({ 762 // in low version, Dev Eco may throw a warning 763 // But you can still build and run the code 764 uiStyle: this.uiStyle 765 }) 766 } 767 .backgroundColor("#F1F3F5") 768 } 769} 770``` 771 772 773 774### 使用@Observed装饰或被声明为状态变量的类对象绑定组件 775 776在开发过程中,会有“重置数据”的场景,将一个新创建的对象赋值给原有的状态变量,实现数据的刷新。如果不注意新创建对象的类型,可能会出现UI不刷新的现象。 777 778```typescript 779@Observed 780class Child { 781 count: number; 782 constructor(count: number) { 783 this.count = count 784 } 785} 786@Observed 787class ChildList extends Array<Child> { 788}; 789@Observed 790class Ancestor { 791 childList: ChildList; 792 constructor(childList: ChildList) { 793 this.childList = childList; 794 } 795 public loadData() { 796 let tempList = [new Child(1), new Child(2), new Child(3), new Child(4), new Child(5)]; 797 this.childList = tempList; 798 } 799 800 public clearData() { 801 this.childList = [] 802 } 803} 804@Component 805struct CompChild { 806 @Link childList: ChildList; 807 @ObjectLink child: Child; 808 809 build() { 810 Row() { 811 Text(this.child.count+'') 812 .height(70) 813 .fontSize(20) 814 .borderRadius({ 815 topLeft: 6, 816 topRight: 6 817 }) 818 .margin({left: 50}) 819 Button('X') 820 .backgroundColor(Color.Red) 821 .onClick(()=>{ 822 let index = this.childList.findIndex((item) => { 823 return item.count === this.child.count 824 }) 825 if (index !== -1) { 826 this.childList.splice(index, 1); 827 } 828 }) 829 .margin({ 830 left: 200, 831 right:30 832 }) 833 } 834 .margin({ 835 top:15, 836 left: 15, 837 right:10, 838 bottom:15 839 }) 840 .borderRadius(6) 841 .backgroundColor(Color.Grey) 842 } 843} 844@Component 845struct CompList { 846 @ObjectLink@Watch('changeChildList') childList: ChildList; 847 848 changeChildList() { 849 console.log('CompList ChildList change'); 850 } 851 852 isRenderCompChild(index: number) : number { 853 console.log("Comp Child is render" + index); 854 return 1; 855 } 856 857 build() { 858 Column() { 859 List() { 860 ForEach(this.childList, (item: Child, index) => { 861 ListItem() { 862 // in low version, Dev Eco may throw a warning 863 // But you can still build and run the code 864 CompChild({ 865 childList: this.childList, 866 child: item 867 }) 868 .opacity(this.isRenderCompChild(index)) 869 } 870 871 }) 872 } 873 .height('70%') 874 } 875 } 876} 877@Component 878struct CompAncestor { 879 @ObjectLink ancestor: Ancestor; 880 881 build() { 882 Column() { 883 // in low version, Dev Eco may throw a warning 884 // But you can still build and run the code 885 CompList({ childList: this.ancestor.childList }) 886 Row() { 887 Button("Clear") 888 .onClick(() => { 889 this.ancestor.clearData() 890 }) 891 .width(100) 892 .margin({right: 50}) 893 Button("Recover") 894 .onClick(() => { 895 this.ancestor.loadData() 896 }) 897 .width(100) 898 } 899 } 900 } 901} 902@Entry 903@Component 904struct Page { 905 @State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)]; 906 @State ancestor: Ancestor = new Ancestor(this.childList) 907 908 build() { 909 Column() { 910 // in low version, Dev Eco may throw a warning 911 // But you can still build and run the code 912 CompAncestor({ ancestor: this.ancestor}) 913 } 914 } 915} 916``` 917 918上述代码运行效果如下。 919 920 921 922上述代码维护了一个ChildList类型的数据源,点击"X"按钮删除一些数据后再点击Recover进行恢复ChildList,发现再次点击"X"按钮进行删除时,UI并没有刷新,同时也没有打印出“CompList ChildList change”的日志。 923 924代码中对数据源childList重新赋值时,是通过Ancestor对象的方法loadData。 925 926```typescript 927 public loadData() { 928 let tempList = [new Child(1), new Child(2), new Child(3), new Child(4), new Child(5)]; 929 this.childList = tempList; 930 } 931``` 932 933在loadData方法中,创建了一个临时的Child类型的数组tempList,并且将Ancestor对象的成员变量的childList指向了tempList。但是这里创建的Child[]类型的数组tempList其实并没有能被观测的能力(也就说它的变化无法主动触发UI刷新)。当它被赋值给childList之后,触发了ForEach的刷新,使得界面完成了重建,但是再次点击删除时,由于此时的childList已经指向了新的tempList代表的数组,并且这个数组并没有被观测的能力,是个静态的量,所以它的更改不会被观测到,也就不会引起UI的刷新。实际上这个时候childList里的数据已经减少了,只是UI没有刷新。 934 935有些开发者会注意到,在Page中初始化定义childList的时候,也是以这样一种方法去进行初始化的。 936 937```typescript 938@State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)]; 939@State ancestor: Ancestor = new Ancestor(this.childList) 940``` 941 942但是由于这里的childList实际上是被@State装饰了,根据当前状态管理的观测能力,尽管右边赋值的是一个Child[]类型的数据,它并没有被@Observed装饰,这里的childList却依然具备了被观测的能力,所以能够正常的触发UI的刷新。当去掉childList的@State的装饰器后,不去重置数据源,也无法通过点击“X”按钮触发刷新。 943 944因此,需要将具有观测能力的类对象绑定组件,来确保当改变这些类对象的内容时,UI能够正常的刷新。 945 946```typescript 947@Observed 948class Child { 949 count: number; 950 constructor(count: number) { 951 this.count = count 952 } 953} 954@Observed 955class ChildList extends Array<Child> { 956}; 957@Observed 958class Ancestor { 959 childList: ChildList; 960 constructor(childList: ChildList) { 961 this.childList = childList; 962 } 963 public loadData() { 964 let tempList = new ChildList(); 965 for (let i = 1; i < 6; i ++) { 966 tempList.push(new Child(i)); 967 } 968 this.childList = tempList; 969 } 970 971 public clearData() { 972 this.childList = [] 973 } 974} 975@Component 976struct CompChild { 977 @Link childList: ChildList; 978 @ObjectLink child: Child; 979 980 build() { 981 Row() { 982 Text(this.child.count+'') 983 .height(70) 984 .fontSize(20) 985 .borderRadius({ 986 topLeft: 6, 987 topRight: 6 988 }) 989 .margin({left: 50}) 990 Button('X') 991 .backgroundColor(Color.Red) 992 .onClick(()=>{ 993 let index = this.childList.findIndex((item) => { 994 return item.count === this.child.count 995 }) 996 if (index !== -1) { 997 this.childList.splice(index, 1); 998 } 999 }) 1000 .margin({ 1001 left: 200, 1002 right:30 1003 }) 1004 } 1005 .margin({ 1006 top:15, 1007 left: 15, 1008 right:10, 1009 bottom:15 1010 }) 1011 .borderRadius(6) 1012 .backgroundColor(Color.Grey) 1013 } 1014} 1015@Component 1016struct CompList { 1017 @ObjectLink@Watch('changeChildList') childList: ChildList; 1018 1019 changeChildList() { 1020 console.log('CompList ChildList change'); 1021 } 1022 1023 isRenderCompChild(index: number) : number { 1024 console.log("Comp Child is render" + index); 1025 return 1; 1026 } 1027 1028 build() { 1029 Column() { 1030 List() { 1031 ForEach(this.childList, (item: Child, index) => { 1032 ListItem() { 1033 // in low version, Dev Eco may throw a warning 1034 // But you can still build and run the code 1035 CompChild({ 1036 childList: this.childList, 1037 child: item 1038 }) 1039 .opacity(this.isRenderCompChild(index)) 1040 } 1041 1042 }) 1043 } 1044 .height('70%') 1045 } 1046 } 1047} 1048@Component 1049struct CompAncestor { 1050 @ObjectLink ancestor: Ancestor; 1051 1052 build() { 1053 Column() { 1054 // in low version, Dev Eco may throw a warning 1055 // But you can still build and run the code 1056 CompList({ childList: this.ancestor.childList }) 1057 Row() { 1058 Button("Clear") 1059 .onClick(() => { 1060 this.ancestor.clearData() 1061 }) 1062 .width(100) 1063 .margin({right: 50}) 1064 Button("Recover") 1065 .onClick(() => { 1066 this.ancestor.loadData() 1067 }) 1068 .width(100) 1069 } 1070 } 1071 } 1072} 1073@Entry 1074@Component 1075struct Page { 1076 @State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)]; 1077 @State ancestor: Ancestor = new Ancestor(this.childList) 1078 1079 build() { 1080 Column() { 1081 // in low version, Dev Eco may throw a warning 1082 // But you can still build and run the code 1083 CompAncestor({ ancestor: this.ancestor}) 1084 } 1085 } 1086} 1087``` 1088 1089上述代码运行效果如下。 1090 1091 1092 1093核心的修改点是将原本Child[]类型的tempList修改为具有被观测能力的ChildList类。 1094 1095```typescript 1096public loadData() { 1097 let tempList = new ChildList(); 1098 for (let i = 1; i < 6; i ++) { 1099 tempList.push(new Child(i)); 1100 } 1101 this.childList = tempList; 1102 } 1103``` 1104 1105ChildList类型在定义的时候使用了@Observed进行装饰,所以用new创建的对象tempList具有被观测的能力,因此在点击“X”按钮删除其中一条内容时,变量childList就能够观测到变化,所以触发了ForEach的刷新,最终UI渲染刷新。 1106 1107## 合理使用ForEach/LazyForEach 1108 1109### 减少使用LazyForEach的重建机制刷新UI 1110 1111开发过程中通常会将[LazyForEach](arkts-rendering-control-lazyforeach.md)和状态变量结合起来使用。 1112 1113```typescript 1114class BasicDataSource implements IDataSource { 1115 private listeners: DataChangeListener[] = []; 1116 private originDataArray: StringData[] = []; 1117 1118 public totalCount(): number { 1119 return 0; 1120 } 1121 1122 public getData(index: number): StringData { 1123 return this.originDataArray[index]; 1124 } 1125 1126 registerDataChangeListener(listener: DataChangeListener): void { 1127 if (this.listeners.indexOf(listener) < 0) { 1128 console.info('add listener'); 1129 this.listeners.push(listener); 1130 } 1131 } 1132 1133 unregisterDataChangeListener(listener: DataChangeListener): void { 1134 const pos = this.listeners.indexOf(listener); 1135 if (pos >= 0) { 1136 console.info('remove listener'); 1137 this.listeners.splice(pos, 1); 1138 } 1139 } 1140 1141 notifyDataReload(): void { 1142 this.listeners.forEach(listener => { 1143 listener.onDataReloaded(); 1144 }) 1145 } 1146 1147 notifyDataAdd(index: number): void { 1148 this.listeners.forEach(listener => { 1149 listener.onDataAdd(index); 1150 }) 1151 } 1152 1153 notifyDataChange(index: number): void { 1154 this.listeners.forEach(listener => { 1155 listener.onDataChange(index); 1156 }) 1157 } 1158 1159 notifyDataDelete(index: number): void { 1160 this.listeners.forEach(listener => { 1161 listener.onDataDelete(index); 1162 }) 1163 } 1164 1165 notifyDataMove(from: number, to: number): void { 1166 this.listeners.forEach(listener => { 1167 listener.onDataMove(from, to); 1168 }) 1169 } 1170} 1171 1172class MyDataSource extends BasicDataSource { 1173 private dataArray: StringData[] = []; 1174 1175 public totalCount(): number { 1176 return this.dataArray.length; 1177 } 1178 1179 public getData(index: number): StringData { 1180 return this.dataArray[index]; 1181 } 1182 1183 public addData(index: number, data: StringData): void { 1184 this.dataArray.splice(index, 0, data); 1185 this.notifyDataAdd(index); 1186 } 1187 1188 public pushData(data: StringData): void { 1189 this.dataArray.push(data); 1190 this.notifyDataAdd(this.dataArray.length - 1); 1191 } 1192 1193 public reloadData(): void { 1194 this.notifyDataReload(); 1195 } 1196} 1197 1198class StringData { 1199 message: string; 1200 imgSrc: Resource; 1201 constructor(message: string, imgSrc: Resource) { 1202 this.message = message; 1203 this.imgSrc = imgSrc; 1204 } 1205} 1206 1207@Entry 1208@Component 1209struct MyComponent { 1210 private data: MyDataSource = new MyDataSource(); 1211 1212 aboutToAppear() { 1213 for (let i = 0; i <= 9; i++) { 1214 this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon'))); // 在API12及以后的工程中使用app.media.app_icon 1215 } 1216 } 1217 1218 build() { 1219 List({ space: 3 }) { 1220 LazyForEach(this.data, (item: StringData, index: number) => { 1221 ListItem() { 1222 Column() { 1223 Text(item.message).fontSize(20) 1224 .onAppear(() => { 1225 console.info("text appear:" + item.message); 1226 }) 1227 Image(item.imgSrc) 1228 .width(100) 1229 .height(100) 1230 .onAppear(() => { 1231 console.info("image appear"); 1232 }) 1233 }.margin({ left: 10, right: 10 }) 1234 } 1235 .onClick(() => { 1236 item.message += '0'; 1237 this.data.reloadData(); 1238 }) 1239 }, (item: StringData, index: number) => JSON.stringify(item)) 1240 }.cachedCount(5) 1241 } 1242} 1243``` 1244 1245上述代码运行效果如下。 1246 1247 1248 1249可以观察到在点击更改message之后,图片“闪烁”了一下,同时输出了组件的onAppear日志,这说明组件进行了重建。这是因为在更改message之后,导致LazyForEach中这一项的key值发生了变化,使得LazyForEach在reloadData的时候将这一项ListItem进行了重建。Text组件仅仅更改显示的内容却发生了重建,而不是更新。而尽管Image组件没有需要重新绘制的内容,但是因为触发LazyForEach的重建,会使得同样位于ListItem下的Image组件重新创建。 1250 1251当前LazyForEach与状态变量都能触发UI的刷新,两者的性能开销是不一样的。使用LazyForEach刷新会对组件进行重建,如果包含了多个组件,则会产生比较大的性能开销。使用状态变量刷新会对组件进行刷新,具体到状态变量关联的组件上,相对于LazyForEach的重建来说,范围更小更精确。因此,推荐使用状态变量来触发LazyForEach中的组件刷新,这就需要使用自定义组件。 1252 1253```typescript 1254class BasicDataSource implements IDataSource { 1255 private listeners: DataChangeListener[] = []; 1256 private originDataArray: StringData[] = []; 1257 1258 public totalCount(): number { 1259 return 0; 1260 } 1261 1262 public getData(index: number): StringData { 1263 return this.originDataArray[index]; 1264 } 1265 1266 registerDataChangeListener(listener: DataChangeListener): void { 1267 if (this.listeners.indexOf(listener) < 0) { 1268 console.info('add listener'); 1269 this.listeners.push(listener); 1270 } 1271 } 1272 1273 unregisterDataChangeListener(listener: DataChangeListener): void { 1274 const pos = this.listeners.indexOf(listener); 1275 if (pos >= 0) { 1276 console.info('remove listener'); 1277 this.listeners.splice(pos, 1); 1278 } 1279 } 1280 1281 notifyDataReload(): void { 1282 this.listeners.forEach(listener => { 1283 listener.onDataReloaded(); 1284 }) 1285 } 1286 1287 notifyDataAdd(index: number): void { 1288 this.listeners.forEach(listener => { 1289 listener.onDataAdd(index); 1290 }) 1291 } 1292 1293 notifyDataChange(index: number): void { 1294 this.listeners.forEach(listener => { 1295 listener.onDataChange(index); 1296 }) 1297 } 1298 1299 notifyDataDelete(index: number): void { 1300 this.listeners.forEach(listener => { 1301 listener.onDataDelete(index); 1302 }) 1303 } 1304 1305 notifyDataMove(from: number, to: number): void { 1306 this.listeners.forEach(listener => { 1307 listener.onDataMove(from, to); 1308 }) 1309 } 1310} 1311 1312class MyDataSource extends BasicDataSource { 1313 private dataArray: StringData[] = []; 1314 1315 public totalCount(): number { 1316 return this.dataArray.length; 1317 } 1318 1319 public getData(index: number): StringData { 1320 return this.dataArray[index]; 1321 } 1322 1323 public addData(index: number, data: StringData): void { 1324 this.dataArray.splice(index, 0, data); 1325 this.notifyDataAdd(index); 1326 } 1327 1328 public pushData(data: StringData): void { 1329 this.dataArray.push(data); 1330 this.notifyDataAdd(this.dataArray.length - 1); 1331 } 1332} 1333 1334@Observed 1335class StringData { 1336 @Track message: string; 1337 @Track imgSrc: Resource; 1338 constructor(message: string, imgSrc: Resource) { 1339 this.message = message; 1340 this.imgSrc = imgSrc; 1341 } 1342} 1343 1344@Entry 1345@Component 1346struct MyComponent { 1347 @State data: MyDataSource = new MyDataSource(); 1348 1349 aboutToAppear() { 1350 for (let i = 0; i <= 9; i++) { 1351 this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon'))); // 在API12及以后的工程中使用app.media.app_icon 1352 } 1353 } 1354 1355 build() { 1356 List({ space: 3 }) { 1357 LazyForEach(this.data, (item: StringData, index: number) => { 1358 ListItem() { 1359 // in low version, Dev Eco may throw a warning 1360 // But you can still build and run the code 1361 ChildComponent({data: item}) 1362 } 1363 .onClick(() => { 1364 item.message += '0'; 1365 }) 1366 }, (item: StringData, index: number) => index.toString()) 1367 }.cachedCount(5) 1368 } 1369} 1370 1371@Component 1372struct ChildComponent { 1373 @ObjectLink data: StringData 1374 build() { 1375 Column() { 1376 Text(this.data.message).fontSize(20) 1377 .onAppear(() => { 1378 console.info("text appear:" + this.data.message) 1379 }) 1380 Image(this.data.imgSrc) 1381 .width(100) 1382 .height(100) 1383 }.margin({ left: 10, right: 10 }) 1384 } 1385} 1386``` 1387 1388上述代码运行效果如下。 1389 1390 1391 1392可以观察到UI能够正常刷新,图片没有“闪烁”,且没有输出日志信息,说明没有对Text组件和Image组件进行重建。 1393 1394这是因为使用自定义组件之后,可以通过@Observed和@ObjectLink配合去直接更改自定义组件内的状态变量实现刷新,而不需要利用LazyForEach进行重建。使用[@Track装饰器](arkts-track.md)分别装饰StringData类型中的message和imgSrc属性可以使更新范围进一步缩小到指定的Text组件。 1395 1396### 在ForEach中使用自定义组件搭配对象数组 1397 1398开发过程中经常会使用对象数组和[ForEach](arkts-rendering-control-foreach.md)结合起来使用,但是写法不当的话会出现UI不刷新的情况。 1399 1400```typescript 1401@Observed 1402class StyleList extends Array<TextStyle> { 1403}; 1404@Observed 1405class TextStyle { 1406 fontSize: number; 1407 1408 constructor(fontSize: number) { 1409 this.fontSize = fontSize; 1410 } 1411} 1412@Entry 1413@Component 1414struct Page { 1415 @State styleList: StyleList = new StyleList(); 1416 aboutToAppear() { 1417 for (let i = 15; i < 50; i++) 1418 this.styleList.push(new TextStyle(i)); 1419 } 1420 build() { 1421 Column() { 1422 Text("Font Size List") 1423 .fontSize(50) 1424 .onClick(() => { 1425 for (let i = 0; i < this.styleList.length; i++) { 1426 this.styleList[i].fontSize++; 1427 } 1428 console.log("change font size"); 1429 }) 1430 List() { 1431 ForEach(this.styleList, (item: TextStyle) => { 1432 ListItem() { 1433 Text("Hello World") 1434 .fontSize(item.fontSize) 1435 } 1436 }) 1437 } 1438 } 1439 } 1440} 1441``` 1442 1443上述代码运行效果如下。 1444 1445 1446 1447由于ForEach中生成的item是一个常量,因此当点击改变item中的内容时,没有办法观测到UI刷新,尽管日志表面item中的值已经改变了(这体现在打印了“change font size”的日志)。因此,需要使用自定义组件,配合@ObjectLink来实现观测的能力。 1448 1449```typescript 1450@Observed 1451class StyleList extends Array<TextStyle> { 1452}; 1453@Observed 1454class TextStyle { 1455 fontSize: number; 1456 1457 constructor(fontSize: number) { 1458 this.fontSize = fontSize; 1459 } 1460} 1461@Component 1462struct TextComponent { 1463 @ObjectLink textStyle: TextStyle; 1464 build() { 1465 Text("Hello World") 1466 .fontSize(this.textStyle.fontSize) 1467 } 1468} 1469@Entry 1470@Component 1471struct Page { 1472 @State styleList: StyleList = new StyleList(); 1473 aboutToAppear() { 1474 for (let i = 15; i < 50; i++) 1475 this.styleList.push(new TextStyle(i)); 1476 } 1477 build() { 1478 Column() { 1479 Text("Font Size List") 1480 .fontSize(50) 1481 .onClick(() => { 1482 for (let i = 0; i < this.styleList.length; i++) { 1483 this.styleList[i].fontSize++; 1484 } 1485 console.log("change font size"); 1486 }) 1487 List() { 1488 ForEach(this.styleList, (item: TextStyle) => { 1489 ListItem() { 1490 // in low version, Dev Eco may throw a warning 1491 // But you can still build and run the code 1492 TextComponent({ textStyle: item}) 1493 } 1494 }) 1495 } 1496 } 1497 } 1498} 1499``` 1500 1501上述代码的运行效果如下。 1502 1503 1504 1505使用@ObjectLink接受传入的item后,使得TextComponent组件内的textStyle变量具有了被观测的能力。在父组件更改styleList中的值时,由于@ObjectLink是引用传递,所以会观测到styleList每一个数据项的地址指向的对应item的fontSize的值被改变,因此触发UI的刷新。 1506 1507这是一个较为实用的使用状态管理进行刷新的开发方式。 1508 1509 1510 1511<!--no_check-->