• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 自定义声明式节点 (BuilderNode)
2
3<!--Kit: ArkUI-->
4<!--Subsystem: ArkUI-->
5<!--Owner: @xiang-shouxing-->
6<!--Designer: @xiang-shouxing-->
7<!--Tester: @sally__-->
8<!--Adviser: @HelloCrease-->
9
10## 概述
11
12自定义声明式节点 ([BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md))提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过[全局自定义构建函数](../ui/state-management/arkts-builder.md#全局自定义构建函数)@Builder定制组件树。组件树的根[FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md)节点可通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取,该节点既可直接由[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)返回并挂载于[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)节点下,亦可在FrameNode树与[RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md)树中嵌入声明式组件,实现混合显示。同时,BuilderNode具备纹理导出功能,导出的纹理可在[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)中实现同层渲染。
13
14由BuilderNode构建的ArkTS组件树,支持与自定义节点(如FrameNode、RenderNode)关联使用,确保了系统组件与自定义节点的混合显示效果。对于需与自定义节点对接的第三方框架,BuilderNode提供了嵌入系统组件的方法。
15
16此外,BuilderNode还提供了组件预创建的能力,能够自定义系统组件的创建开始的时间,在后续业务中实现动态挂载与显示。此功能尤其适用于初始化耗时较长的声明式组件,如[Web](../reference/apis-arkweb/arkts-basic-components-web.md)、[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)等,通过预创建,可以有效减少初始化时间,优化组件加载效率。
17
18![zh-cn_image_builder-node](figures/builder-node.png)
19
20## 基本概念
21
22- 系统组件:组件是UI的必要元素,形成了在界面中的样子,由ArkUI直接提供的称为[系统组件](arkts-ui-development-overview.md)。
23
24- 实体节点:由后端创建的Native节点。
25
26BuilderNode仅可作为叶子节点进行使用。如有更新需要,建议通过BuilderNode中的[update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update)方式触发更新,不建议通过BuilderNode中获取的RenderNode对节点进行修改操作。
27
28> **说明:**
29>
30> - BuilderNode只支持一个由[wrapBuilder](../ui/state-management/arkts-wrapBuilder.md)包装的[全局自定义构建函数](../ui/state-management/arkts-builder.md#全局自定义构建函数)@Builder。
31>
32> - 一个新建的BuilderNode在[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build)之后才能通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取到一个指向根节点的FrameNode对象,否则返回null。
33>
34> - 如果传入的Builder的根节点为语法节点(if/else/foreach/...),需要额外生成一个FrameNode,在节点树中的显示为“BuilderProxyNode”。
35>
36> - 如果BuilderNode通过getFrameNode将节点挂载在另一个FrameNode上,或者将其作为子节点挂载在NodeContainer节点上。则节点中使用父组件的布局约束进行布局。
37>
38> - 如果BuilderNode的FrameNode通过[getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode)形式将自己的节点挂载在RenderNode节点上,由于其FrameNode未上树,其大小默认为0,需要通过构造函数中的[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)显式指定布局约束大小,才能正常显示。
39>
40> - BuilderNode的预加载并不会减少组件的创建时间。Web组件创建的时候需要在内核中加载资源,预创建不能减少Web组件的创建的时间,但是可以让内核进行预加载,减少正式使用时候内核的加载耗时。
41
42## 创建BuilderNode对象
43
44BuilderNode对象为一个模板类,需要在创建的时候指定类型。该类型需要与后续build方法中传入的[WrappedBuilder](../ui/state-management/arkts-wrapBuilder.md)的类型保持一致,否则会存在编译告警导致编译失败。
45
46## 创建组件树
47
48通过BuilderNode的build可以实现组件树的创建。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。
49
50> **说明:**
51>
52> 无状态的UI方法全局@Builder最多拥有一个根节点。
53>
54> build方法中对应的@Builder支持一个参数作为入参。
55>
56> build中对于@Builder嵌套@Builder进行使用的场景,需要保证嵌套的参数与build的中提供的入参一致。
57>
58> 对于@Builder嵌套@Builder进行使用的场景,如果入参类型不一致,则要求增加[BuilderOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildoptions12)字段作为[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build12)的入参。
59>
60> 需要操作BuilderNode中的对象时,需要保证其引用不被回收。当BuilderNode对象被虚拟机回收之后,它的FrameNode、RenderNode对象也会与后端节点解引用。即从BuilderNode中获取的FrameNode对象不对应任何一个节点。
61
62创建离线节点以及组件树,结合FrameNode进行使用。
63
64BuilderNode的根节点直接作为[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)的[makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode)返回值。
65
66```ts
67import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
68
69class Params {
70  text: string = "";
71
72  constructor(text: string) {
73    this.text = text;
74  }
75}
76
77@Builder
78function buildText(params: Params) {
79  Column() {
80    Text(params.text)
81      .fontSize(50)
82      .fontWeight(FontWeight.Bold)
83      .margin({ bottom: 36 })
84  }
85}
86
87class TextNodeController extends NodeController {
88  private textNode: BuilderNode<[Params]> | null = null;
89  private message: string = "DEFAULT";
90
91  constructor(message: string) {
92    super();
93    this.message = message;
94  }
95
96  makeNode(context: UIContext): FrameNode | null {
97    this.textNode = new BuilderNode(context);
98    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
99    return this.textNode.getFrameNode();
100  }
101}
102
103@Entry
104@Component
105struct Index {
106  @State message: string = "hello";
107
108  build() {
109    Row() {
110      Column() {
111        NodeContainer(new TextNodeController(this.message))
112          .width('100%')
113          .height(100)
114          .backgroundColor('#FFF0F0F0')
115      }
116      .width('100%')
117      .height('100%')
118    }
119    .height('100%')
120  }
121}
122```
123
124将BuilderNode与RenderNode进行结合使用。
125
126BuilderNode的RenderNode挂载其它RenderNode下时,需要明确定义[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)的大小作为BuilderNode的布局约束。不推荐通过该方式挂载节点。
127
128```ts
129import { NodeController, BuilderNode, FrameNode, UIContext, RenderNode } from "@kit.ArkUI";
130
131class Params {
132  text: string = "";
133
134  constructor(text: string) {
135    this.text = text;
136  }
137}
138
139@Builder
140function buildText(params: Params) {
141  Column() {
142    Text(params.text)
143      .fontSize(50)
144      .fontWeight(FontWeight.Bold)
145      .margin({ bottom: 36 })
146  }
147}
148
149class TextNodeController extends NodeController {
150  private rootNode: FrameNode | null = null;
151  private textNode: BuilderNode<[Params]> | null = null;
152  private message: string = "DEFAULT";
153
154  constructor(message: string) {
155    super();
156    this.message = message;
157  }
158
159  makeNode(context: UIContext): FrameNode | null {
160    this.rootNode = new FrameNode(context);
161    let renderNode = new RenderNode();
162    renderNode.clipToFrame = false;
163    this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } });
164    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
165    const textRenderNode = this.textNode?.getFrameNode()?.getRenderNode();
166
167    const rootRenderNode = this.rootNode.getRenderNode();
168    if (rootRenderNode !== null) {
169      rootRenderNode.appendChild(renderNode);
170      renderNode.appendChild(textRenderNode);
171    }
172
173    return this.rootNode;
174  }
175}
176
177@Entry
178@Component
179struct Index {
180  @State message: string = "hello";
181
182  build() {
183    Row() {
184      Column() {
185        NodeContainer(new TextNodeController(this.message))
186          .width('100%')
187          .height(100)
188          .backgroundColor('#FFF0F0F0')
189      }
190      .width('100%')
191      .height('100%')
192    }
193    .height('100%')
194  }
195}
196```
197
198## 更新组件树
199
200通过BuilderNode对象的build创建组件树。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。
201
202自定义组件的更新遵循[状态管理](../ui/state-management/arkts-state-management-overview.md)的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为[\@Prop](../ui/state-management/arkts-prop.md)或者[\@ObjectLink](../ui/state-management/arkts-observed-and-objectlink.md)。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。
203
204
205使用update更新BuilderNode中的节点。
206
207使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)触发BuilderNode中节点的全量更新。
208
209更新BuilderNode中的节点。
210
211```ts
212import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
213
214class Params {
215  text: string = "";
216  constructor(text: string) {
217    this.text = text;
218  }
219}
220
221// 自定义组件
222@Component
223struct TextBuilder {
224  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
225  @Prop message: string = "TextBuilder";
226
227  build() {
228    Row() {
229      Column() {
230        Text(this.message)
231          .fontSize(50)
232          .fontWeight(FontWeight.Bold)
233          .margin({ bottom: 36 })
234          .backgroundColor(Color.Gray)
235      }
236    }
237  }
238}
239
240@Builder
241function buildText(params: Params) {
242  Column() {
243    Text(params.text)
244      .fontSize(50)
245      .fontWeight(FontWeight.Bold)
246      .margin({ bottom: 36 })
247    TextBuilder({ message: params.text }) // 自定义组件
248  }
249}
250
251class TextNodeController extends NodeController {
252  private textNode: BuilderNode<[Params]> | null = null;
253  private message: string = "";
254
255  constructor(message: string) {
256    super()
257    this.message = message
258  }
259
260  makeNode(context: UIContext): FrameNode | null {
261    this.textNode = new BuilderNode(context);
262    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
263    return this.textNode.getFrameNode();
264  }
265
266  update(message: string) {
267    if (this.textNode !== null) {
268      // 调用update进行更新。
269      this.textNode.update(new Params(message));
270    }
271  }
272}
273
274@Entry
275@Component
276struct Index {
277  @State message: string = "hello";
278  private textNodeController: TextNodeController = new TextNodeController(this.message);
279  private count = 0;
280
281  build() {
282    Row() {
283      Column() {
284        NodeContainer(this.textNodeController)
285          .width('100%')
286          .height(200)
287          .backgroundColor('#FFF0F0F0')
288        Button('Update')
289          .onClick(() => {
290            this.count += 1;
291            const message = "Update " + this.count.toString();
292            this.textNodeController.update(message);
293          })
294      }
295      .width('100%')
296      .height('100%')
297    }
298    .height('100%')
299  }
300}
301```
302
303## 解除实体节点引用关系
304
305由于BuilderNode对应的是后端的实体节点,正常的内存释放依赖前端对象的回收。如果期望直接释放后端的节点对象,则可以通过调用[dispose](../reference/apis-arkui/js-apis-arkui-builderNode.md#dispose12)与实体节点解除引用关系,此时持有的前端BuilderNode对象不再影响实体节点的生命周期。
306
307> **说明:**
308>
309> 当BuilderNode对象调用dispose之后,不仅BuilderNode对象与后端实体节点解除引用关系,BuilderNode中的FrameNode与RenderNode也会同步和实体节点解除引用关系。
310>
311> 若前端对象BuilderNode无法释放,容易导致内存泄漏。建议在不再需要对该BuilderNode对象进行操作时,开发者应主动调用dispose释放后端节点,以减少引用关系的复杂性,降低内存泄漏的风险。
312
313## 注入触摸事件
314
315BuilderNode中提供了[postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent),可以通过该接口向BuilderNode中绑定的组件注入[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md),实现事件的模拟转发。
316
317通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。
318
319向BuilderNode中的Column组件转发另一个Column接收的事件,即点击下方的Column组件,上方的Column组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。
320
321```ts
322import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
323
324class Params {
325  text: string = "this is a text";
326}
327
328@Builder
329function ButtonBuilder(params: Params) {
330  Column() {
331    Button(`button ` + params.text)
332      .borderWidth(2)
333      .backgroundColor(Color.Orange)
334      .width("100%")
335      .height("100%")
336      .gesture(
337        TapGesture()
338          .onAction((event: GestureEvent) => {
339            console.info("TapGesture");
340          })
341      )
342  }
343  .width(500)
344  .height(300)
345  .backgroundColor(Color.Gray)
346}
347
348class MyNodeController extends NodeController {
349  private rootNode: BuilderNode<[Params]> | null = null;
350  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder);
351
352  makeNode(uiContext: UIContext): FrameNode | null {
353    this.rootNode = new BuilderNode(uiContext);
354    this.rootNode.build(this.wrapBuilder, { text: "this is a string" })
355    return this.rootNode.getFrameNode();
356  }
357
358  postTouchEvent(touchEvent: TouchEvent): void {
359    if (this.rootNode == null) {
360      return;
361    }
362    let result = this.rootNode.postTouchEvent(touchEvent);
363    console.info("result " + result);
364  }
365}
366
367@Entry
368@Component
369struct MyComponent {
370  private nodeController: MyNodeController = new MyNodeController();
371
372  build() {
373    Column() {
374      NodeContainer(this.nodeController)
375        .height(300)
376        .width(500)
377      Column()
378        .width(500)
379        .height(300)
380        .backgroundColor(Color.Pink)
381        .onTouch((event) => {
382          if (event != undefined) {
383            this.nodeController.postTouchEvent(event);
384          }
385        })
386    }
387  }
388}
389```
390
391## BuilderNode内的BuilderProxyNode导致树结构发生变化
392
393若传入的Builder的根节点为语法节点(if/else/foreach/…)或自定义组件,将额外生成一个FrameNode,在节点树中显示为“BuilderProxyNode”,这会导致树结构变化,影响某些测试的传递过程。
394
395在以下示例中,Column和Row绑定了触摸事件,同时Column设置了[hitTestBehavior](../reference/apis-arkui/arkui-ts/ts-universal-attributes-hit-test-behavior.md#hittestbehavior)属性为[HitTestMode.Transparent](../reference/apis-arkui/arkui-ts/ts-appendix-enums.md#hittestmode9)。然而,由于生成了BuilderProxyNode,且BuilderProxyNode无法设置属性,因此在触摸Column时,Column的触摸测试无法传递到Row上。
396
397![BuilderNode_BuilderProxyNode_1](figures/BuilderNode_BuilderProxyNode_1.png)
398
399```ts
400import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI';
401
402@Component
403struct BlueRowComponent {
404  build() {
405    Row() {
406      Row() {
407      }
408      .width('100%')
409      .height('200vp')
410      .backgroundColor(0xFF2787D9)
411      .onTouch((event: TouchEvent) => {
412        // 触摸绿色Column,蓝色Row的触摸事件不触发
413        console.info("blue touched: " + event.type);
414      })
415    }
416  }
417}
418
419@Component
420struct GreenColumnComponent {
421  build() {
422    Column() {
423    }
424    .width('100%')
425    .height('100vp')
426    .backgroundColor(0xFF17A98D)
427    .hitTestBehavior(HitTestMode.Transparent)
428    .onTouch((event: TouchEvent) => {
429      console.info("green touched: " + event.type);
430    })
431  }
432}
433
434@Builder
435function buildBlueRow() {
436  // Builder直接挂载自定义组件,生成BuilderProxyNode
437  BlueRowComponent()
438}
439
440@Builder
441function buildGreenColumn() {
442  // Builder直接挂载自定义组件,生成BuilderProxyNode
443  GreenColumnComponent()
444}
445
446class MyNodeController extends NodeController {
447  makeNode(uiContext: UIContext): FrameNode | null {
448    const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer');
449
450    const blueRowNode = new BuilderNode(uiContext);
451    blueRowNode.build(wrapBuilder(buildBlueRow));
452
453    const greenColumnNode = new BuilderNode(uiContext);
454    greenColumnNode.build(wrapBuilder(buildGreenColumn));
455
456    // greenColumnNode覆盖在blueRowNode上
457    relativeContainer.appendChild(blueRowNode.getFrameNode());
458    relativeContainer.appendChild(greenColumnNode.getFrameNode());
459
460    return relativeContainer;
461  }
462}
463
464@Entry
465@Component
466struct Index {
467  build() {
468    Column() {
469      NodeContainer(new MyNodeController())
470    }
471  }
472}
473```
474
475在上述场景中,若要实现触摸测试的传递,可以使用一个容器组件包裹语法节点或自定义组件,以避免生成BuilderProxyNode,并将容器组件的hitTestBehavior设置为HitTestMode.Transparent,从而向兄弟节点传递触摸测试。
476
477![BuilderNode_BuilderProxyNode_2](figures/BuilderNode_BuilderProxyNode_2.png)
478
479```ts
480import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI';
481
482@Component
483struct BlueRowComponent {
484  build() {
485    Row() {
486      Row() {
487      }
488      .width('100%')
489      .height('200vp')
490      .backgroundColor(0xFF2787D9)
491      .onTouch((event: TouchEvent) => {
492        // 触摸绿色Column,蓝色Row的触摸事件触发
493        console.info("blue touched: " + event.type);
494      })
495    }
496  }
497}
498
499@Component
500struct GreenColumnComponent {
501  build() {
502    Column() {
503    }
504    .width('100%')
505    .height('100vp')
506    .backgroundColor(0xFF17A98D)
507    .hitTestBehavior(HitTestMode.Transparent)
508    .onTouch((event: TouchEvent) => {
509      console.info("green touched: " + event.type);
510    })
511  }
512}
513
514@Builder
515function buildBlueRow() {
516  // Builder直接挂载自定义组件,生成BuilderProxyNode
517  BlueRowComponent()
518}
519
520@Builder
521function buildGreenColumn() {
522  // Builder根节点为容器组件,不会生成BuilderProxyNode,可以设置属性
523  Stack() {
524    GreenColumnComponent()
525  }
526  .hitTestBehavior(HitTestMode.Transparent)
527}
528
529class MyNodeController extends NodeController {
530  makeNode(uiContext: UIContext): FrameNode | null {
531    const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer');
532
533    const blueRowNode = new BuilderNode(uiContext);
534    blueRowNode.build(wrapBuilder(buildBlueRow));
535
536    const greenColumnNode = new BuilderNode(uiContext);
537    greenColumnNode.build(wrapBuilder(buildGreenColumn));
538
539    // greenColumnNode覆盖在blueRowNode上
540    relativeContainer.appendChild(blueRowNode.getFrameNode());
541    relativeContainer.appendChild(greenColumnNode.getFrameNode());
542
543    return relativeContainer;
544  }
545}
546
547@Entry
548@Component
549struct Index {
550  build() {
551    Column() {
552      NodeContainer(new MyNodeController())
553    }
554  }
555}
556```
557
558此外,对于自定义组件,可以直接设置属性,此时将额外生成节点__Common__,自定义组件的属性将挂载于__Common__上,同样能够实现上述效果。
559
560![BuilderNode_BuilderProxyNode_3](figures/BuilderNode_BuilderProxyNode_3.png)
561
562```ts
563import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI';
564
565@Component
566struct BlueRowComponent {
567  build() {
568    Row() {
569      Row() {
570      }
571      .width('100%')
572      .height('200vp')
573      .backgroundColor(0xFF2787D9)
574      .onTouch((event: TouchEvent) => {
575        // 触摸绿色Column,蓝色Row的触摸事件触发
576        console.info("blue touched: " + event.type);
577      })
578    }
579  }
580}
581
582@Component
583struct GreenColumnComponent {
584  build() {
585    Column() {
586    }
587    .width('100%')
588    .height('100vp')
589    .backgroundColor(0xFF17A98D)
590    .hitTestBehavior(HitTestMode.Transparent)
591    .onTouch((event: TouchEvent) => {
592      console.info("green touched: " + event.type);
593    })
594  }
595}
596
597@Builder
598function buildBlueRow() {
599  // Builder直接挂载自定义组件,生成BuilderProxyNode
600  BlueRowComponent()
601}
602
603@Builder
604function buildGreenColumn() {
605  // 给自定义组件设置属性生成__Common__节点,Builder根节点为__Common__节点,不会生成BuilderProxyNode
606  GreenColumnComponent()
607    .hitTestBehavior(HitTestMode.Transparent)
608}
609
610class MyNodeController extends NodeController {
611  makeNode(uiContext: UIContext): FrameNode | null {
612    const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer');
613
614    const blueRowNode = new BuilderNode(uiContext);
615    blueRowNode.build(wrapBuilder(buildBlueRow));
616
617    const greenColumnNode = new BuilderNode(uiContext);
618    greenColumnNode.build(wrapBuilder(buildGreenColumn));
619
620    // greenColumnNode覆盖在blueRowNode上
621    relativeContainer.appendChild(blueRowNode.getFrameNode());
622    relativeContainer.appendChild(greenColumnNode.getFrameNode());
623
624    return relativeContainer;
625  }
626}
627
628@Entry
629@Component
630struct Index {
631  build() {
632    Column() {
633      NodeContainer(new MyNodeController())
634    }
635  }
636}
637```
638
639## BuilderNode调用reuse和recycle接口实现节点复用能力
640
641调用[reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12)接口和[recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12)接口,将复用和回收事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点内部的自定义组件的复用。
642
643以下面的Demo为例,被复用的自定义组件ReusableChildComponent可以传递复用和回收事件到其下的自定义组件ChildComponent3,但无法传递给自定义组件ChildComponent2,因为被BuilderNode所隔断。因此需要主动调用BuilderNode的reuse和recycle接口,将复用和回收事件传递给自定义组件ChildComponent2,以达成复用效果。
644
645![zh-cn_image_reuse-recycle](figures/reuse-recycle.png)
646
647
648```ts
649import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI";
650
651const TEST_TAG: string = "Reuse+Recycle";
652
653class MyDataSource {
654  private dataArray: string[] = [];
655  private listener: DataChangeListener | null = null
656
657  public totalCount(): number {
658    return this.dataArray.length;
659  }
660
661  public getData(index: number) {
662    return this.dataArray[index];
663  }
664
665  public pushData(data: string) {
666    this.dataArray.push(data);
667  }
668
669  public reloadListener(): void {
670    this.listener?.onDataReloaded();
671  }
672
673  public registerDataChangeListener(listener: DataChangeListener): void {
674    this.listener = listener;
675  }
676
677  public unregisterDataChangeListener(): void {
678    this.listener = null;
679  }
680}
681
682class Params {
683  item: string = '';
684
685  constructor(item: string) {
686    this.item = item;
687  }
688}
689
690@Builder
691function buildNode(param: Params = new Params("hello")) {
692  Row() {
693    Text(`C${param.item} -- `)
694    ChildComponent2({ item: param.item }) //该自定义组件在BuilderNode中无法被正确复用
695  }
696}
697
698class MyNodeController extends NodeController {
699  public builderNode: BuilderNode<[Params]> | null = null;
700  public item: string = "";
701
702  makeNode(uiContext: UIContext): FrameNode | null {
703    if (this.builderNode == null) {
704      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
705      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
706    }
707    return this.builderNode.getFrameNode();
708  }
709}
710
711// 被回收复用的自定义组件,其状态变量会更新,而子自定义组件ChildComponent3中的状态变量也会更新,但BuilderNode会阻断这一传递过程
712@Reusable
713@Component
714struct ReusableChildComponent {
715  @Prop item: string = '';
716  @Prop switch: string = '';
717  private controller: MyNodeController = new MyNodeController();
718
719  aboutToAppear() {
720    this.controller.item = this.item;
721  }
722
723  aboutToRecycle(): void {
724    console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`);
725
726    // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ChildComponent2,完成复用
727    if (this.switch === 'open') {
728      this.controller?.builderNode?.recycle();
729    }
730  }
731
732  aboutToReuse(params: object): void {
733    console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`);
734
735    // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ChildComponent2,完成复用
736    if (this.switch === 'open') {
737      this.controller?.builderNode?.reuse(params);
738    }
739  }
740
741  build() {
742    Row() {
743      Text(`A${this.item}--`)
744      ChildComponent3({ item: this.item })
745      NodeContainer(this.controller);
746    }
747  }
748}
749
750@Component
751struct ChildComponent2 {
752  @Prop item: string = "false";
753
754  aboutToReuse(params: Record<string, object>) {
755    console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`);
756  }
757
758  aboutToRecycle(): void {
759    console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`);
760  }
761
762  build() {
763    Row() {
764      Text(`D${this.item}`)
765        .fontSize(20)
766        .backgroundColor(Color.Yellow)
767        .margin({ left: 10 })
768    }.margin({ left: 10, right: 10 })
769  }
770}
771
772@Component
773struct ChildComponent3 {
774  @Prop item: string = "false";
775
776  aboutToReuse(params: Record<string, object>) {
777    console.log(`${TEST_TAG} ChildComponent3 aboutToReuse ${JSON.stringify(params)}`);
778  }
779
780  aboutToRecycle(): void {
781    console.log(`${TEST_TAG} ChildComponent3 aboutToRecycle ${this.item}`);
782  }
783
784  build() {
785    Row() {
786      Text(`B${this.item}`)
787        .fontSize(20)
788        .backgroundColor(Color.Yellow)
789        .margin({ left: 10 })
790    }.margin({ left: 10, right: 10 })
791  }
792}
793
794
795@Entry
796@Component
797struct Index {
798  @State data: MyDataSource = new MyDataSource();
799
800  aboutToAppear() {
801    for (let i = 0; i < 100; i++) {
802      this.data.pushData(i.toString());
803    }
804  }
805
806  build() {
807    Column() {
808      List({ space: 3 }) {
809        LazyForEach(this.data, (item: string) => {
810          ListItem() {
811            ReusableChildComponent({
812              item: item,
813              switch: 'open' // 将open改为close可观察到,BuilderNode不通过reuse和recycle接口传递复用时,BuilderNode内部的自定义组件的行为表现
814            })
815          }
816        }, (item: string) => item)
817      }
818      .width('100%')
819      .height('100%')
820    }
821  }
822}
823```
824
825
826## BuilderNode在子自定义组件中使用@Reusable装饰器
827
828BuilderNode节点的复用机制与使用[@Reusable](./state-management/arkts-reusable.md)装饰器的自定义组件的复用机制会相互冲突。因此,当BuilderNode的子节点为自定义组件时,不支持该自定义组件使用@Reusable装饰器标记,否则将导致应用程序触发JSCrash。若需要使用@Reusable装饰器,应使用一个普通自定义组件包裹该自定义组件。
829
830在下面的示例中,ReusableChildComponent作为BuilderNode的子自定义组件,无法标记为@Reusable。通过ChildComponent2对其包裹,ReusableChildComponent可以使用@Reusable装饰器标记。
831
832![BuilderNode-Reusable](figures/BuilderNode-Reusable.png)
833
834```ts
835import { FrameNode, NodeController, BuilderNode, UIContext } from '@kit.ArkUI';
836
837const TEST_TAG: string = "Reusable";
838
839class Params {
840  item: string = '';
841
842  constructor(item: string) {
843    this.item = item;
844  }
845}
846
847@Builder
848function buildNode(param: Params = new Params("Hello")) {
849  ChildComponent2({ item: param.item })
850  // 如果直接使用ReusableChildComponent,则会编译报错
851  // ReusableChildComponent({ item: param.item })
852}
853
854class MyNodeController extends NodeController {
855  public builderNode: BuilderNode<[Params]> | null = null;
856  public item: string = "";
857
858  constructor(item: string) {
859    super();
860    this.item = item;
861  }
862
863  makeNode(uiContext: UIContext): FrameNode | null {
864    if (this.builderNode == null) {
865      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
866      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
867    }
868    return this.builderNode.getFrameNode();
869  }
870}
871
872// 标记了@Reusable的自定义组件,无法直接被BuilderNode挂载为子节点
873@Reusable
874@Component
875struct ReusableChildComponent {
876  @Prop item: string = '';
877
878  aboutToReuse(params: object): void {
879    console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`);
880  }
881
882  aboutToRecycle(): void {
883    console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`);
884  }
885
886  build() {
887    Text(`A--${this.item}`)
888  }
889}
890
891// 未标记@Reusable的自定义组件
892@Component
893struct ChildComponent2 {
894  @Prop item: string = "";
895
896  aboutToReuse(params: Record<string, object>) {
897    console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`);
898  }
899
900  aboutToRecycle(): void {
901    console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`);
902  }
903
904  build() {
905    ReusableChildComponent({ item: this.item })
906  }
907}
908
909
910@Entry
911@Component
912struct Index {
913  @State controller: MyNodeController = new MyNodeController("Child");
914
915  build() {
916    Column() {
917      NodeContainer(this.controller)
918    }
919    .width('100%')
920    .height('100%')
921  }
922}
923```
924
925## 通过系统环境变化更新节点
926
927使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)来监听[系统环境变化](../reference/apis-ability-kit/js-apis-app-ability-configuration.md)事件,以触发节点的全量更新。
928
929> **说明:**
930>
931> updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。
932
933```ts
934import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
935import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit';
936
937class Params {
938  text: string = ""
939
940  constructor(text: string) {
941    this.text = text;
942  }
943}
944
945// 自定义组件
946@Component
947struct TextBuilder {
948  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
949  @Prop message: string = "TextBuilder";
950
951  build() {
952    Row() {
953      Column() {
954        Text(this.message)
955          .fontSize(50)
956          .fontWeight(FontWeight.Bold)
957          .margin({ bottom: 36 })
958          .fontColor($r(`app.color.text_color`))
959          .backgroundColor($r(`app.color.start_window_background`))
960      }
961    }
962  }
963}
964
965@Builder
966function buildText(params: Params) {
967  Column() {
968    Text(params.text)
969      .fontSize(50)
970      .fontWeight(FontWeight.Bold)
971      .margin({ bottom: 36 })
972      .fontColor($r(`app.color.text_color`))
973    TextBuilder({ message: params.text }) // 自定义组件
974  }.backgroundColor($r(`app.color.start_window_background`))
975}
976
977class TextNodeController extends NodeController {
978  private textNode: BuilderNode<[Params]> | null = null;
979  private message: string = "";
980
981  constructor(message: string) {
982    super()
983    this.message = message;
984  }
985
986  makeNode(context: UIContext): FrameNode | null {
987    return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null;
988  }
989
990  createNode(context: UIContext) {
991    this.textNode = new BuilderNode(context);
992    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
993    builderNodeMap.push(this.textNode);
994  }
995
996  deleteNode() {
997    let node = builderNodeMap.pop();
998    node?.dispose();
999  }
1000
1001  update(message: string) {
1002    if (this.textNode !== null) {
1003      // 调用update进行更新。
1004      this.textNode.update(new Params(message));
1005    }
1006  }
1007}
1008
1009// 记录创建的自定义节点对象
1010const builderNodeMap: Array<BuilderNode<[Params]>> = new Array();
1011
1012function updateColorMode() {
1013  builderNodeMap.forEach((value, index) => {
1014    // 通知BuilderNode环境变量改变
1015    value.updateConfiguration();
1016  })
1017}
1018
1019@Entry
1020@Component
1021struct Index {
1022  @State message: string = "hello"
1023  private textNodeController: TextNodeController = new TextNodeController(this.message);
1024  private count = 0;
1025
1026  aboutToAppear(): void {
1027    let environmentCallback: EnvironmentCallback = {
1028      onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {
1029        console.info('onMemoryLevel');
1030      },
1031      onConfigurationUpdated: (config: Configuration): void => {
1032        console.info('onConfigurationUpdated ' + JSON.stringify(config));
1033        updateColorMode();
1034      }
1035    }
1036    // 注册监听回调
1037    this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback);
1038    //创建自定义节点并添加至map
1039    this.textNodeController.createNode(this.getUIContext());
1040  }
1041
1042  aboutToDisappear(): void {
1043    //移除map中的引用,并将自定义节点释放
1044    this.textNodeController.deleteNode();
1045  }
1046
1047  build() {
1048    Row() {
1049      Column() {
1050        NodeContainer(this.textNodeController)
1051          .width('100%')
1052          .height(200)
1053          .backgroundColor('#FFF0F0F0')
1054        Button('Update')
1055          .onClick(() => {
1056            this.count += 1;
1057            const message = "Update " + this.count.toString();
1058            this.textNodeController.update(message);
1059          })
1060      }
1061      .width('100%')
1062      .height('100%')
1063    }
1064    .height('100%')
1065  }
1066}
1067```
1068
1069## 跨页面复用注意事项
1070
1071在使用[路由](../reference/apis-arkui/arkts-apis-uicontext-router.md)接口[router.replaceUrl](../reference/apis-arkui/arkts-apis-uicontext-router.md#replaceurl)、[router.back](../reference/apis-arkui/arkts-apis-uicontext-router.md#back)、[router.clear](../reference/apis-arkui/arkts-apis-uicontext-router.md#clear)、[router.replaceNamedRoute](../reference/apis-arkui/arkts-apis-uicontext-router.md#replacenamedroute)操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以[router.replaceNamedRoute](../reference/apis-arkui/arkts-apis-uicontext-router.md#replacenamedroute)为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为false。
1072
1073```ts
1074// ets/pages/Index.ets
1075import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
1076import "ets/pages/PageTwo"
1077
1078@Builder
1079function buildText() {
1080  // @Builder中使用语法节点生成BuilderProxyNode
1081  if (true) {
1082    MyComponent()
1083  }
1084}
1085
1086@Component
1087struct MyComponent {
1088  @StorageLink("isShowText") isShowText: boolean = true;
1089
1090  build() {
1091    if (this.isShowText) {
1092      Column() {
1093        Text("BuilderNode Reuse")
1094          .fontSize(36)
1095          .fontWeight(FontWeight.Bold)
1096          .padding(16)
1097      }
1098    }
1099  }
1100}
1101
1102class TextNodeController extends NodeController {
1103  private rootNode: FrameNode | null = null;
1104  private textNode: BuilderNode<[]> | null = null;
1105
1106  makeNode(context: UIContext): FrameNode | null {
1107    this.rootNode = new FrameNode(context);
1108
1109    if (AppStorage.has("textNode")) {
1110      // 复用AppStorage中的BuilderNode
1111      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
1112      const parent = this.textNode.getFrameNode()?.getParent();
1113      if (parent) {
1114        parent.removeChild(this.textNode.getFrameNode());
1115      }
1116    } else {
1117      this.textNode = new BuilderNode(context);
1118      this.textNode.build(wrapBuilder<[]>(buildText));
1119      // 将创建的BuilderNode存入AppStorage
1120      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
1121    }
1122    this.rootNode.appendChild(this.textNode.getFrameNode());
1123
1124    return this.rootNode;
1125  }
1126}
1127
1128@Entry({ routeName: "myIndex" })
1129@Component
1130struct Index {
1131  aboutToAppear(): void {
1132    AppStorage.setOrCreate<boolean>("isShowText", true);
1133  }
1134
1135  build() {
1136    Row() {
1137      Column() {
1138        NodeContainer(new TextNodeController())
1139          .width('100%')
1140          .backgroundColor('#FFF0F0F0')
1141        Button('Router pageTwo')
1142          .onClick(() => {
1143            // 改变AppStorage中的状态变量触发Text节点的重新创建
1144            AppStorage.setOrCreate<boolean>("isShowText", false);
1145
1146            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
1147          })
1148          .margin({ top: 16 })
1149      }
1150      .width('100%')
1151      .height('100%')
1152      .padding(16)
1153    }
1154    .height('100%')
1155  }
1156}
1157```
1158
1159PageTwo的实现如下:
1160
1161```ts
1162// ets/pages/PageTwo.ets
1163// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失
1164import "ets/pages/Index"
1165
1166@Entry({ routeName: "pageTwo" })
1167@Component
1168struct PageTwo {
1169  build() {
1170    Column() {
1171      Button('Router replace to index')
1172        .onClick(() => {
1173          this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" });
1174        })
1175    }
1176    .height('100%')
1177    .width('100%')
1178    .alignItems(HorizontalAlign.Center)
1179    .padding(16)
1180  }
1181}
1182```
1183
1184![BuilderNode Reuse Example](./figures/builder_node_reuse.gif)
1185
1186在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。
1187
1188API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。
1189
1190```ts
1191// ets/pages/Index.ets
1192import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
1193import "ets/pages/PageTwo"
1194
1195@Builder
1196function buildText() {
1197  // @Builder中使用语法节点生成BuilderProxyNode
1198  if (true) {
1199    MyComponent()
1200  }
1201}
1202
1203@Component
1204struct MyComponent {
1205  @StorageLink("isShowText") isShowText: boolean = true;
1206
1207  build() {
1208    if (this.isShowText) {
1209      Column() {
1210        Text("BuilderNode Reuse")
1211          .fontSize(36)
1212          .fontWeight(FontWeight.Bold)
1213          .padding(16)
1214      }
1215    }
1216  }
1217}
1218
1219class TextNodeController extends NodeController {
1220  private rootNode: FrameNode | null = null;
1221  private textNode: BuilderNode<[]> | null = null;
1222
1223  makeNode(context: UIContext): FrameNode | null {
1224    this.rootNode = new FrameNode(context);
1225
1226    if (AppStorage.has("textNode")) {
1227      // 复用AppStorage中的BuilderNode
1228      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
1229      const parent = this.textNode.getFrameNode()?.getParent();
1230      if (parent) {
1231        parent.removeChild(this.textNode.getFrameNode());
1232      }
1233    } else {
1234      this.textNode = new BuilderNode(context);
1235      this.textNode.build(wrapBuilder<[]>(buildText));
1236      // 将创建的BuilderNode存入AppStorage
1237      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
1238    }
1239    this.rootNode.appendChild(this.textNode.getFrameNode());
1240
1241    return this.rootNode;
1242  }
1243}
1244
1245@Entry({ routeName: "myIndex" })
1246@Component
1247struct Index {
1248  aboutToAppear(): void {
1249    AppStorage.setOrCreate<boolean>("isShowText", true);
1250  }
1251
1252  build() {
1253    Row() {
1254      Column() {
1255        NodeContainer(new TextNodeController())
1256          .width('100%')
1257          .backgroundColor('#FFF0F0F0')
1258        Button('Router pageTwo')
1259          .onClick(() => {
1260            // 改变AppStorage中的状态变量触发Text节点的重新创建
1261            AppStorage.setOrCreate<boolean>("isShowText", false);
1262            // 将BuilderNode从AppStorage中移除
1263            AppStorage.delete("textNode");
1264
1265            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
1266          })
1267          .margin({ top: 16 })
1268      }
1269      .width('100%')
1270      .height('100%')
1271      .padding(16)
1272    }
1273    .height('100%')
1274  }
1275}
1276```
1277
1278
1279## BuilderNode中使用LocalStorage
1280
1281从API version 12开始,自定义组件支持接收[LocalStorage](../ui/state-management/arkts-localstorage.md)实例。可以通过[传递LocalStorage实例](../ui/state-management/arkts-localstorage.md#自定义组件接收localstorage实例)来使用LocalStorage相关的装饰器[@LocalStorageProp](../ui/state-management/arkts-localstorage.md#localstorageprop)、[@LocalStorageLink](../ui/state-management/arkts-localstorage.md#localstoragelink)。
1282
1283```ts
1284import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI';
1285
1286let localStorage1: LocalStorage = new LocalStorage();
1287localStorage1.setOrCreate('PropA', 'PropA');
1288
1289let localStorage2: LocalStorage = new LocalStorage();
1290localStorage2.setOrCreate('PropB', 'PropB');
1291
1292@Entry(localStorage1)
1293@Component
1294struct Index {
1295  // 'PropA',和localStorage1中'PropA'的双向同步
1296  @LocalStorageLink('PropA') PropA: string = 'Hello World';
1297  @State count: number = 0;
1298  private controller: NodeController = new MyNodeController(this.count, localStorage2);
1299
1300  build() {
1301    Row() {
1302      Column() {
1303        Text(this.PropA)
1304          .fontSize(50)
1305          .fontWeight(FontWeight.Bold)
1306        // 使用LocalStorage 实例localStorage2
1307        Child({ count: this.count }, localStorage2)
1308        NodeContainer(this.controller)
1309      }
1310      .width('100%')
1311    }
1312    .height('100%')
1313  }
1314}
1315
1316interface Params {
1317  count: number;
1318  localStorage: LocalStorage;
1319}
1320
1321@Builder
1322function CreateChild(params: Params) {
1323  //构造过程中传递localStorage
1324  Child({ count: params.count }, params.localStorage)
1325}
1326
1327class MyNodeController extends NodeController {
1328  private count?: number;
1329  private localStorage ?: LocalStorage;
1330
1331  constructor(count: number, localStorage: LocalStorage) {
1332    super();
1333    this.count = count;
1334    this.localStorage = localStorage;
1335  }
1336
1337  makeNode(uiContext: UIContext): FrameNode | null {
1338    let builderNode = new BuilderNode<[Params]>(uiContext);
1339    //构造过程中传递localStorage
1340    builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage });
1341    return builderNode.getFrameNode();
1342  }
1343}
1344
1345@Component
1346struct Child {
1347  @Prop count: number;
1348  //  'Hello World',和localStorage2中'PropB'的双向同步,如果localStorage2中没有'PropB',则使用默认值'Hello World'
1349  @LocalStorageLink('PropB') PropB: string = 'Hello World';
1350
1351  build() {
1352    Text(this.PropB)
1353      .fontSize(50)
1354      .fontWeight(FontWeight.Bold)
1355  }
1356}
1357```
1358
1359## 查询当前BuilderNode是否解除引用
1360
1361前端节点均绑定有相应的后端实体节点,当节点调用dispose接口解除绑定后,再次调用接口可能会出现crash、返回默认值的情况。
1362
1363从API version 20开始,使用[isDisposed](../reference/apis-arkui/js-apis-arkui-builderNode.md#isdisposed20)接口查询当前BuilderNode对象是否已解除与后端实体节点的引用关系,从而可以在操作节点前检查其有效性,避免潜在风险。
1364
1365```ts
1366import { NodeController, FrameNode, BuilderNode } from '@kit.ArkUI';
1367
1368@Builder
1369function buildText() {
1370  Text("Test")
1371    .fontSize(20)
1372    .fontWeight(FontWeight.Bold)
1373}
1374
1375class MyNodeController extends NodeController {
1376  private rootNode: FrameNode | null = null;
1377  private builderNode: BuilderNode<[]> | null = null;
1378
1379  makeNode(uiContext: UIContext): FrameNode | null {
1380    this.rootNode = new FrameNode(uiContext);
1381    this.rootNode.commonAttribute.width(100).height(100).backgroundColor(Color.Pink);
1382    this.builderNode = new BuilderNode<[]>(uiContext);
1383    this.builderNode.build(wrapBuilder<[]>(buildText));
1384
1385    // 挂载BuilderNode
1386    this.rootNode.appendChild(this.builderNode.getFrameNode());
1387    return this.rootNode;
1388  }
1389
1390  disposeBuilderNode() {
1391    // 解除BuilderNode与后端实体节点的引用关系
1392    this.builderNode?.dispose();
1393  }
1394
1395  isDisposed() : string {
1396    if (this.builderNode !== null) {
1397      // 查询BuilderNode是否解除引用
1398      if (this.builderNode.isDisposed()) {
1399        return 'builderNode isDisposed is true';
1400      }
1401      else {
1402        return 'builderNode isDisposed is false';
1403      }
1404    }
1405    return 'builderNode is null';
1406  }
1407}
1408
1409@Entry
1410@Component
1411struct Index {
1412  @State text: string = ''
1413  private myNodeController: MyNodeController = new MyNodeController();
1414
1415  build() {
1416    Column({ space: 4 }) {
1417      NodeContainer(this.myNodeController)
1418      Button('BuilderNode dispose')
1419        .onClick(() => {
1420          this.myNodeController.disposeBuilderNode();
1421          this.text = '';
1422        })
1423        .width(200)
1424        .height(50)
1425      Button('BuilderNode isDisposed')
1426        .onClick(() => {
1427          this.text = this.myNodeController.isDisposed();
1428        })
1429        .width(200)
1430        .height(50)
1431      Text(this.text)
1432        .fontSize(25)
1433    }
1434    .width('100%')
1435    .height('100%')
1436  }
1437}
1438```
1439
1440## 设置BuilderNode继承冻结能力
1441
1442ArkUI支持[自定义组件冻结](./state-management/arkts-custom-components-freeze.md),该功能冻结非激活状态组件的刷新能力。当组件处于非激活状态时,即便其绑定状态变量发生变化,也不会触发组件UI的重新渲染,从而减少复杂UI场景的刷新负载。
1443
1444从API version 20开始,BuilderNode节点可以通过[inheritFreezeOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#inheritfreezeoptions20)接口继承父自定义组件(即从该BuilderNode节点向上查找的第一个自定义组件)的冻结策略。当BuilderNode节点继承父自定义组件的冻结策略时,若父自定义组件的冻结策略设置为开启组件冻结(即[freezeWhenInactive](../reference/apis-arkui/arkui-ts/ts-custom-component-parameter.md#componentoptions)选项设为true),则BuilderNode节点在不活跃时将会冻结,当切换至活跃状态时解冻,并使用缓存的数据更新节点。
1445
1446BuilderNode节点只有通过以下方式上下树时,才会根据该节点是否继承父自定义组件的冻结策略,来更新自己的冻结策略:
1447
1448| 类 | 接口 |
1449| -------- | -------- |
1450| [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md) | [appendChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#appendchild12)、[insertChildAfter](../reference/apis-arkui/js-apis-arkui-frameNode.md#insertchildafter12)、[removeChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#removechild12)、[clearChildren](../reference/apis-arkui/js-apis-arkui-frameNode.md#clearchildren12)、[addComponentContent](../reference/apis-arkui/js-apis-arkui-frameNode.md#addcomponentcontent12) |
1451| [NodeContent](../reference/apis-arkui/js-apis-arkui-NodeContent.md) | [addFrameNode](../reference/apis-arkui/js-apis-arkui-NodeContent.md#addframenode12)、[removeFrameNode](../reference/apis-arkui/js-apis-arkui-NodeContent.md#removeframenode12) |
1452| [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md) | [makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode) |
1453| [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) | [appendChild](../reference/apis-arkui/js-apis-arkui-renderNode.md#appendchild)、[insertChildAfter](../reference/apis-arkui/js-apis-arkui-renderNode.md#insertchildafter)、[removeChild](../reference/apis-arkui/js-apis-arkui-renderNode.md#removechild)、[clearChildren](../reference/apis-arkui/js-apis-arkui-renderNode.md#clearchildren) |
1454| [NodeAdaper](../reference/apis-arkui/js-apis-arkui-frameNode.md#nodeadapter12) | 节点通过[懒加载](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)方式上下树时 |
1455
1456> **说明:**
1457>
1458> 当BuilderNode节点设置为继承父自定义组件的冻结策略时,BuilderNode节点的冻结策略将与其上层最近的自定义组件或BuilderNode节点的冻结策略保持一致。
1459>
1460> 当BuilderNode节点被冻结时,调用[update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update)接口不会触发节点的更新,等其被解冻时再更新节点。
1461
1462```ts
1463
1464import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
1465
1466class Params {
1467  count: number = 0;
1468
1469  constructor(count: number) {
1470    this.count = count;
1471  }
1472}
1473
1474@Builder
1475function buildText(params: Params) {
1476
1477  Column() {
1478    TextBuilder({ message: params.count })
1479  }
1480}
1481
1482class TextNodeController extends NodeController {
1483  private rootNode: FrameNode | null = null;
1484  private textNode: BuilderNode<[Params]> | null = null;
1485  private count: number = 0;
1486
1487  makeNode(context: UIContext): FrameNode | null {
1488    this.rootNode = new FrameNode(context);
1489    this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } });
1490    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.count));
1491    this.textNode.inheritFreezeOptions(true); // 设置BuilderNode的冻结继承状态为true
1492    if (this.rootNode !== null) {
1493      this.rootNode.appendChild(this.textNode.getFrameNode()); // 将BuilderNode上树
1494    }
1495    return this.rootNode;
1496  }
1497
1498  update(): void {
1499    if (this.textNode !== null) {
1500      this.count += 1;
1501      this.textNode.update(new Params(this.count)); // 更新BuilderNode中的数据,可以触发Log
1502    }
1503
1504  }
1505}
1506
1507const textNodeController: TextNodeController = new TextNodeController();
1508
1509@Entry
1510@Component
1511struct MyNavigationTestStack {
1512  @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack();
1513  @State message: number = 0;
1514  @State logNumber: number = 0;
1515
1516  @Builder
1517  PageMap(name: string) {
1518    if (name === 'pageOne') {
1519      pageOneStack({ message: this.message, logNumber: this.logNumber })
1520    } else if (name === 'pageTwo') {
1521      pageTwoStack({ message: this.message, logNumber: this.logNumber })
1522    }
1523  }
1524
1525  build() {
1526    Column() {
1527      Button('update builderNode') // 点击更新BuildrNode
1528        .onClick(() => {
1529          textNodeController.update();
1530        })
1531      Navigation(this.pageInfo) {
1532        Column() {
1533          Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
1534            .width('80%')
1535            .height(40)
1536            .margin(20)
1537            .onClick(() => {
1538              this.pageInfo.pushPath({ name: 'pageOne' }); // 将name指定的NavDestination页面信息入栈
1539            })
1540        }
1541      }.title('NavIndex')
1542      .navDestination(this.PageMap)
1543      .mode(NavigationMode.Stack)
1544    }
1545  }
1546}
1547
1548@Component
1549struct pageOneStack {
1550  @Consume('pageInfo') pageInfo: NavPathStack;
1551  @State index: number = 1;
1552  @Link message: number;
1553  @Link logNumber: number;
1554
1555  build() {
1556    NavDestination() {
1557      Column() {
1558        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
1559        Button('Next Page', { stateEffect: true, type: ButtonType.Capsule })
1560          .width('80%')
1561          .height(40)
1562          .margin(20)
1563          .onClick(() => {
1564            this.pageInfo.pushPathByName('pageTwo', null);
1565          })
1566        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
1567          .width('80%')
1568          .height(40)
1569          .margin(20)
1570          .onClick(() => {
1571            this.pageInfo.pop();
1572          })
1573      }.width('100%').height('100%')
1574    }.title('pageOne')
1575    .onBackPressed(() => {
1576      this.pageInfo.pop();
1577      return true;
1578    })
1579  }
1580}
1581
1582@Component
1583struct pageTwoStack {
1584  @Consume('pageInfo') pageInfo: NavPathStack;
1585  @State index: number = 2;
1586  @Link message: number;
1587  @Link logNumber: number;
1588
1589  build() {
1590    NavDestination() {
1591      Column() {
1592        NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber })
1593        Text('BuilderNode处于冻结')
1594          .fontWeight(FontWeight.Bold)
1595          .margin({ top: 48, bottom: 48 })
1596        Button('Back Page', { stateEffect: true, type: ButtonType.Capsule })
1597          .width('80%')
1598          .height(40)
1599          .margin(20)
1600          .onClick(() => {
1601            this.pageInfo.pop();
1602          })
1603      }.width('100%').height('100%')
1604    }.title('pageTwo')
1605    .onBackPressed(() => {
1606      this.pageInfo.pop();
1607      return true;
1608    })
1609  }
1610}
1611
1612@Component({ freezeWhenInactive: true }) // 设置冻结策略为不活跃冻结
1613struct NavigationContentMsgStack {
1614  @Link message: number;
1615  @Link index: number;
1616  @Link logNumber: number;
1617
1618  build() {
1619    Column() {
1620      if (this.index === 1) {
1621        NodeContainer(textNodeController)
1622      }
1623    }
1624  }
1625}
1626
1627@Component({ freezeWhenInactive: true }) // 设置冻结策略为不活跃冻结
1628struct TextBuilder {
1629  @Prop @Watch("info") message: number = 0;
1630
1631  info(): void {
1632    console.info(`freeze-test TextBuilder message callback ${this.message}`); // 根据message内容变化来打印日志来判断是否冻结
1633  }
1634
1635  build() {
1636    Row() {
1637      Column() {
1638        Text(`文本更新次数: ${this.message}`)
1639          .fontWeight(FontWeight.Bold)
1640          .margin({ top: 48, bottom: 48 })
1641      }
1642    }
1643  }
1644}
1645```
1646
1647![inheritFreezeOptions](figures/builderNode_inheritFreezeOptions.gif)
1648
1649## 设置BuilderNode支持内部@Consume接收外部的@Provide数据
1650
1651从API version 20开始,通过配置BuildOptions参数,BuilderNode内部自定义组件的[@Consume](./state-management/arkts-provide-and-consume.md)支持接收所在页面的[@Provide](./state-management/arkts-provide-and-consume.md)数据。
1652
1653参见[示例代码](../reference/apis-arkui/js-apis-arkui-builderNode.md#示例7buildernode支持内部consume接收外部的provide数据)。
1654
1655## BuilderNode结合ArkWeb组件实现预渲染页面
1656
1657预渲染适用于Web页面启动与跳转等场景。通过结合BuilderNode,可以将ArkWeb组件提前进行离线预渲染,组件不会即时挂载至页面,而是在需要时通过NodeController动态挂载与显示。此举能够提高页面切换的流畅度及用户体验。
1658
1659> **说明**
1660>
1661> 访问在线网页时需添加网络权限:ohos.permission.INTERNET,具体申请方式请参考[声明权限](../security/AccessToken/declare-permissions.md)。
1662
16631. 创建载体Ability,并创建Web组件。
1664   ```ts
1665   // 载体Ability
1666   // EntryAbility.ets
1667   import { createNWeb } from "../pages/common";
1668   import { UIAbility } from '@kit.AbilityKit';
1669   import { window } from '@kit.ArkUI';
1670
1671   export default class EntryAbility extends UIAbility {
1672     onWindowStageCreate(windowStage: window.WindowStage): void {
1673       windowStage.loadContent('pages/Index', (err, data) => {
1674         // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建。
1675         createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
1676         if (err.code) {
1677           return;
1678         }
1679       });
1680     }
1681   }
1682   ```
16832. 创建NodeContainer和对应的NodeController,渲染后台Web组件。
1684
1685    ```ts
1686    // 创建NodeController。
1687    // common.ets
1688    import { UIContext } from '@kit.ArkUI';
1689    import { webview } from '@kit.ArkWeb';
1690    import { NodeController, BuilderNode, Size, FrameNode }  from '@kit.ArkUI';
1691    // @Builder中为动态组件的具体组件内容。
1692    // Data为入参封装类。
1693    class Data{
1694      url: string = 'https://www.example.com';
1695      controller: WebviewController = new webview.WebviewController();
1696    }
1697    // 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染。
1698    let shouldInactive: boolean = true;
1699    @Builder
1700    function WebBuilder(data:Data) {
1701      Column() {
1702        Web({ src: data.url, controller: data.controller })
1703          .onPageBegin(() => {
1704            // 调用onActive,开启渲染。
1705            data.controller.onActive();
1706          })
1707          .onFirstMeaningfulPaint(() =>{
1708            if (!shouldInactive) {
1709              return;
1710            }
1711            // 在预渲染完成时触发,停止渲染。
1712            data.controller.onInactive();
1713            shouldInactive = false;
1714          })
1715          .width("100%")
1716          .height("100%")
1717      }
1718    }
1719    let wrap = wrapBuilder<Data[]>(WebBuilder);
1720    // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。
1721    export class myNodeController extends NodeController {
1722      private rootnode: BuilderNode<Data[]> | null = null;
1723      // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中。
1724      // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。
1725      makeNode(uiContext: UIContext): FrameNode | null {
1726        console.info(" uicontext is undifined : "+ (uiContext === undefined));
1727        if (this.rootnode != null) {
1728          // 返回FrameNode节点。
1729          return this.rootnode.getFrameNode();
1730        }
1731        // 返回null控制动态组件脱离绑定节点。
1732        return null;
1733      }
1734      // 当布局大小发生变化时进行回调。
1735      aboutToResize(size: Size) {
1736        console.info("aboutToResize width : " + size.width  +  " height : " + size.height );
1737      }
1738      // 当controller对应的NodeContainer在Appear的时候进行回调。
1739      aboutToAppear() {
1740        console.info("aboutToAppear");
1741        // 切换到前台后,不需要停止渲染。
1742        shouldInactive = false;
1743      }
1744      // 当controller对应的NodeContainer在Disappear的时候进行回调。
1745      aboutToDisappear() {
1746        console.info("aboutToDisappear");
1747      }
1748      // 此函数为自定义函数,可作为初始化函数使用。
1749      // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容。
1750      initWeb(url:string, uiContext:UIContext, control:WebviewController) {
1751        if(this.rootnode != null){
1752          return;
1753        }
1754        // 创建节点,需要uiContext。
1755        this.rootnode = new BuilderNode(uiContext);
1756        // 创建动态Web组件。
1757        this.rootnode.build(wrap, { url:url, controller:control });
1758      }
1759    }
1760    // 创建Map保存所需要的NodeController。
1761    let NodeMap:Map<string, myNodeController | undefined> = new Map();
1762    // 创建Map保存所需要的WebViewController。
1763    let controllerMap:Map<string, WebviewController | undefined> = new Map();
1764    // 初始化需要UIContext 需在Ability获取。
1765    export const createNWeb = (url: string, uiContext: UIContext) => {
1766      // 创建NodeController。
1767      let baseNode = new myNodeController();
1768      let controller = new webview.WebviewController() ;
1769      // 初始化自定义Web组件。
1770      baseNode.initWeb(url, uiContext, controller);
1771      controllerMap.set(url, controller);
1772      NodeMap.set(url, baseNode);
1773    }
1774    // 自定义获取NodeController接口。
1775    export const getNWeb = (url : string) : myNodeController | undefined => {
1776      return NodeMap.get(url);
1777    }
1778    ```
17793. 通过NodeContainer使用已经预渲染的页面。
1780
1781    ```ts
1782    // 使用NodeController的Page页。
1783    // Index.ets
1784    import { createNWeb, getNWeb } from "./common";
1785
1786    @Entry
1787    @Component
1788    struct Index {
1789      build() {
1790        Row() {
1791          Column() {
1792            // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode。
1793            // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示。
1794            NodeContainer(getNWeb("https://www.example.com"))
1795              .height("90%")
1796              .width("100%")
1797          }
1798          .width('100%')
1799        }
1800        .height('100%')
1801      }
1802    }
1803    ```
1804