1# 自定义组件冻结功能 2 3当@ComponentV2装饰的自定义组件处于非激活状态时,状态变量将不响应更新,即@Monitor不会调用,状态变量关联的节点不会刷新。通过freezeWhenInactive属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:页面路由,TabContent,Navigation。 4 5在阅读本文档前,开发者需要了解\@ComponentV2基本语法。建议提前阅读:[\@ComponentV2](./arkts-new-componentV2.md)。 6 7> **说明:** 8> 9> 从API version 12开始,支持@ComponentV2装饰的自定义组件冻结功能。 10> 11> 和@Component的组件冻结不同, @ComponentV2装饰的自定义组件不支持LazyForEach场景下的缓存节点组件冻结。 12 13 14## 当前支持的场景 15 16### 页面路由 17 18> **说明:** 19> 20> 本示例使用了router进行页面跳转,建议开发者使用组件导航(Navigation)代替页面路由(router)来实现页面切换。Navigation提供了更多的功能和更灵活的自定义能力。请参考[使用Navigation的组件冻结用例](#navigation)。 21 22- 当页面1调用router.pushUrl接口跳转到页面2时,页面1为隐藏不可见状态,此时如果更新页面1中的状态变量,不会触发页面1刷新。 23图示如下: 24 25 26 27页面1: 28 29```ts 30import { router } from '@kit.ArkUI'; 31 32@ObservedV2 33export class Book { 34 @Trace name: string = "100"; 35 36 constructor(page: string) { 37 this.name = page; 38 } 39} 40 41@Entry 42@ComponentV2({ freezeWhenInactive: true }) 43export struct Page1 { 44 @Local bookTest: Book = new Book("A Midsummer Night’s Dream"); 45 46 @Monitor("bookTest.name") 47 onMessageChange(monitor: IMonitor) { 48 console.log(`The book name change from ${monitor.value()?.before} to ${monitor.value()?.now}`); 49 } 50 51 build() { 52 Column() { 53 Text(`Book name is ${this.bookTest.name}`).fontSize(25) 54 Button('changeBookName').fontSize(25) 55 .onClick(() => { 56 this.bookTest.name = "The Old Man and the Sea"; 57 }) 58 Button('go to next page').fontSize(25) 59 .onClick(() => { 60 router.pushUrl({ url: 'pages/Page2' }); 61 setTimeout(() => { 62 this.bookTest = new Book("Jane Austen oPride and Prejudice"); 63 }, 1000) 64 }) 65 } 66 } 67} 68``` 69 70页面2: 71 72```ts 73import { router } from '@kit.ArkUI'; 74 75@Entry 76@ComponentV2 77struct Page2 { 78 build() { 79 Column() { 80 Text(`This is the page2`).fontSize(25) 81 Button('Back') 82 .onClick(() => { 83 router.back(); 84 }) 85 } 86 } 87} 88``` 89 90在上面的示例中: 91 921.点击页面1中的Button “changeBookName”,bookTest变量的name属性改变,@Monitor中注册的方法onMessageChange会被调用。 93 942.点击页面1中的Button “go to next page”,跳转到页面2,然后延迟1s更新状态变量“bookTest”。在更新“bookTest”的时候,已经跳转到页面2,页面1处于inactive状态,状态变量`@Local bookTest`将不响应更新,其@Monitor不会调用,状态变量关联的节点不会刷新。 95trace如下: 96 97 98 99 1003.点击“back”,页面2被销毁,页面1的状态由inactive变为active。状态变量“bookTest”的更新被观察到,@Monitor中注册的方法onMessageChange被调用,对应的Text显示内容改变。 101 102 103 104 105### TabContent 106 107- 对Tabs中当前不可见的TabContent进行冻结,不会触发组件的更新。 108 109- 需要注意的是:在首次渲染的时候,Tab只会创建当前正在显示的TabContent,当切换全部的TabContent后,TabContent才会被全部创建。 110 111图示如下: 112 113 114 115```ts 116@Entry 117@ComponentV2 118struct TabContentTest { 119 @Local message: number = 0; 120 @Local data: number[] = [0, 1]; 121 122 build() { 123 Row() { 124 Column() { 125 Button('change message').onClick(() => { 126 this.message++; 127 }) 128 129 Tabs() { 130 ForEach(this.data, (item: number) => { 131 TabContent() { 132 FreezeChild({ message: this.message, index: item }) 133 }.tabBar(`tab${item}`) 134 }, (item: number) => item.toString()) 135 } 136 } 137 .width('100%') 138 } 139 .height('100%') 140 } 141} 142 143@ComponentV2({ freezeWhenInactive: true }) 144struct FreezeChild { 145 @Param message: number = 0; 146 @Param index: number = 0; 147 148 @Monitor('message') onMessageUpdated(mon: IMonitor) { 149 console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`); 150 } 151 152 build() { 153 Text("message" + `${this.message}, index: ${this.index}`) 154 .fontSize(50) 155 .fontWeight(FontWeight.Bold) 156 } 157} 158``` 159 160在上面的示例中: 161 1621.点击“change message”更改message的值,当前正在显示的TabContent组件中的@Monitor中注册的方法onMessageUpdated被触发。 163 1642.点击TabBar“tab1”切换到另外的TabContent,TabContent状态由inactive变为active,对应的@Monitor中注册的方法onMessageUpdated被触发。 165 1663.再次点击“change message”更改message的值,仅当前显示的TabContent子组件中的@Monitor中注册的方法onMessageUpdated被触发。其他inactive的TabContent组件不会触发@Monitor。 167 168 169 170 171### Navigation 172 173- 对当前不可见的页面进行冻结,不会触发组件的更新,当返回该页面时,触发@Monitor回调进行刷新。 174 175```ts 176@Entry 177@ComponentV2 178struct MyNavigationTestStack { 179 @Provider('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 180 @Local message: number = 0; 181 182 @Monitor('message') info() { 183 console.info(`freeze-test MyNavigation message callback ${this.message}`); 184 } 185 186 @Builder 187 PageMap(name: string) { 188 if (name === 'pageOne') { 189 pageOneStack({ message: this.message }) 190 } else if (name === 'pageTwo') { 191 pageTwoStack({ message: this.message }) 192 } else if (name === 'pageThree') { 193 pageThreeStack({ message: this.message }) 194 } 195 } 196 197 build() { 198 Column() { 199 Button('change message') 200 .onClick(() => { 201 this.message++; 202 }) 203 Navigation(this.pageInfo) { 204 Column() { 205 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 206 .onClick(() => { 207 this.pageInfo.pushPath({ name: 'pageOne' }); //将name指定的NavDestination页面信息入栈 208 }) 209 } 210 }.title('NavIndex') 211 .navDestination(this.PageMap) 212 .mode(NavigationMode.Stack) 213 } 214 } 215} 216 217@ComponentV2 218struct pageOneStack { 219 @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 220 @Local index: number = 1; 221 @Param message: number = 0; 222 223 build() { 224 NavDestination() { 225 Column() { 226 NavigationContentMsgStack({ message: this.message, index: this.index }) 227 Text("cur stack size:" + `${this.pageInfo.size()}`) 228 .fontSize(30) 229 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 230 .onClick(() => { 231 this.pageInfo.pushPathByName('pageTwo', null); 232 }) 233 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 234 .onClick(() => { 235 this.pageInfo.pop(); 236 }) 237 }.width('100%').height('100%') 238 }.title('pageOne') 239 .onBackPressed(() => { 240 this.pageInfo.pop(); 241 return true; 242 }) 243 } 244} 245 246@ComponentV2 247struct pageTwoStack { 248 @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 249 @Local index: number = 2; 250 @Param message: number = 0; 251 252 build() { 253 NavDestination() { 254 Column() { 255 NavigationContentMsgStack({ message: this.message, index: this.index }) 256 Text("cur stack size:" + `${this.pageInfo.size()}`) 257 .fontSize(30) 258 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 259 .onClick(() => { 260 this.pageInfo.pushPathByName('pageThree', null); 261 }) 262 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 263 .onClick(() => { 264 this.pageInfo.pop(); 265 }) 266 } 267 }.title('pageTwo') 268 .onBackPressed(() => { 269 this.pageInfo.pop(); 270 return true; 271 }) 272 } 273} 274 275@ComponentV2 276struct pageThreeStack { 277 @Consumer('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 278 @Local index: number = 3; 279 @Param message: number = 0; 280 281 build() { 282 NavDestination() { 283 Column() { 284 NavigationContentMsgStack({ message: this.message, index: this.index }) 285 Text("cur stack size:" + `${this.pageInfo.size()}`) 286 .fontSize(30) 287 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 288 .height(40) 289 .onClick(() => { 290 this.pageInfo.pushPathByName('pageOne', null); 291 }) 292 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 293 .height(40) 294 .onClick(() => { 295 this.pageInfo.pop(); 296 }) 297 } 298 }.title('pageThree') 299 .onBackPressed(() => { 300 this.pageInfo.pop(); 301 return true; 302 }) 303 } 304} 305 306@ComponentV2({ freezeWhenInactive: true }) 307struct NavigationContentMsgStack { 308 @Param message: number = 0; 309 @Param index: number = 0; 310 311 @Monitor('message') info() { 312 console.info(`freeze-test NavigationContent message callback ${this.message}`); 313 console.info(`freeze-test ---- called by content ${this.index}`); 314 } 315 316 build() { 317 Column() { 318 Text("msg:" + `${this.message}`) 319 .fontSize(30) 320 } 321 } 322} 323``` 324 325在上面的示例中: 326 3271.点击“change message”更改message的值,当前正在显示的MyNavigationTestStack组件中的@Monitor中注册的方法info被触发。 328 3292.点击“Next Page”切换到PageOne,创建pageOneStack节点。 330 3313.再次点击“change message”更改message的值,仅pageOneStack中的NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。 332 3334.再次点击“Next Page”切换到PageTwo,创建pageTwoStack节点。pageOneStack节点状态由active变为inactive 334 3355.再次点击“change message”更改message的值,仅pageTwoStack中的NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。Navigation路由栈中非栈顶的NavDestination中的子自定义组件,将是inactive状态。@Monitor方法不会触发。 336 3376.再次点击“Next Page”切换到PageThree,创建pageThreeStack节点。pageTwoStack节点状态由active变为inactive 338 3397.再次点击“change message”更改message的值,仅pageThreeStack中的NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。Navigation路由栈中非栈顶的NavDestination中的子自定义组件,将是inactive状态。@Monitor方法不会触发。 340 3418.点击“Back Page”回到PageTwo,此时,pageTwoStack节点状态由inactive变为active,其NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。 342 3439.再次点击“Back Page”回到PageOne,此时,pageOneStack节点状态由inactive变为active,其NavigationContentMsgStack子组件中的@Monitor中注册的方法info被触发。 344 34510.再次点击“Back Page”回到初始页。 346 347 348 349 350## 限制条件 351如下面的例子所示,FreezeBuildNode中使用了自定义节点[BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md)。BuilderNode可以通过命令式动态挂载组件,而组件冻结又是强依赖父子关系来通知是否开启组件冻结。如果父组件使用组件冻结,且组件树的中间层级上又启用了BuilderNode,则BuilderNode的子组件将无法被冻结。 352 353``` 354import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 355 356// 定义一个Params类,用于传递参数 357@ObservedV2 358class Params { 359 // 单例模式,确保只有一个Params实例 360 static singleton_: Params; 361 362 // 获取Params实例的方法 363 static instance() { 364 if (!Params.singleton_) { 365 Params.singleton_ = new Params(0); 366 } 367 return Params.singleton_; 368 } 369 370 // 使用@Trace装饰器装饰message属性,以便跟踪其变化 371 @Trace message: string = "Hello"; 372 index: number = 0; 373 374 constructor(index: number) { 375 this.index = index; 376 } 377} 378 379// 定义一个buildNodeChild组件,它包含一个message属性和一个index属性 380@ComponentV2 381struct buildNodeChild { 382 // 使用Params实例作为storage属性 383 storage: Params = Params.instance(); 384 @Param index: number = 0; 385 386 // 使用@Monitor装饰器监听storage.message的变化 387 @Monitor("storage.message") 388 onMessageChange(monitor: IMonitor) { 389 console.log(`FreezeBuildNode buildNodeChild message callback func ${this.storage.message}, index:${this.index}`); 390 } 391 392 build() { 393 Text(`buildNode Child message: ${this.storage.message}`).fontSize(30) 394 } 395} 396 397// 定义一个buildText函数,它接收一个Params参数并构建一个Column组件 398@Builder 399function buildText(params: Params) { 400 Column() { 401 buildNodeChild({ index: params.index }) 402 } 403} 404 405class TextNodeController extends NodeController { 406 private textNode: BuilderNode<[Params]> | null = null; 407 private index: number = 0; 408 409 // 构造函数接收一个index参数 410 constructor(index: number) { 411 super(); 412 this.index = index; 413 } 414 415 // 创建并返回一个FrameNode 416 makeNode(context: UIContext): FrameNode | null { 417 this.textNode = new BuilderNode(context); 418 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.index)); 419 return this.textNode.getFrameNode(); 420 } 421} 422 423// 定义一个Index组件,它包含一个message属性和一个data数组 424@Entry 425@ComponentV2 426struct Index { 427 // 使用Params实例作为storage属性 428 storage: Params = Params.instance(); 429 private data: number[] = [0, 1]; 430 431 build() { 432 Row() { 433 Column() { 434 Button("change").fontSize(30) 435 .onClick(() => { 436 this.storage.message += 'a'; 437 }) 438 439 Tabs() { 440 // 使用Repeat重复渲染TabContent组件 441 Repeat<number>(this.data) 442 .each((obj: RepeatItem<number>) => { 443 TabContent() { 444 FreezeBuildNode({ index: obj.item }) 445 .margin({ top: 20 }) 446 }.tabBar(`tab${obj.item}`) 447 }) 448 .key((item: number) => item.toString()) 449 } 450 } 451 } 452 .width('100%') 453 .height('100%') 454 } 455} 456 457// 定义一个FreezeBuildNode组件,它包含一个message属性和一个index属性 458@ComponentV2({ freezeWhenInactive: true }) 459struct FreezeBuildNode { 460 // 使用Params实例作为storage属性 461 storage: Params = Params.instance(); 462 @Param index: number = 0; 463 464 // 使用@Monitor装饰器监听storage.message的变化 465 @Monitor("storage.message") 466 onMessageChange(monitor: IMonitor) { 467 console.log(`FreezeBuildNode message callback func ${this.storage.message}, index: ${this.index}`); 468 } 469 470 build() { 471 NodeContainer(new TextNodeController(this.index)) 472 .width('100%') 473 .height('100%') 474 .backgroundColor('#FFF0F0F0') 475 } 476} 477``` 478 479点击Button("change")。改变message的值,当前正在显示的TabContent组件中的@Watch中注册的方法onMessageUpdated被触发。未显示的TabContent中的BuilderNode节点下组件的@Watch方法onMessageUpdated也被触发,并没有被冻结。 480 481