识别和解决组件树中节点双挂问题
===============

#### 问题现象

在组件树中，当一个自定义节点被同时挂载到多个父节点下，会导致UI渲染异常（如UI不显示、白块等）。该问题可通过以下方式确认：

1.  hilog日志确认：
    
    Add \[id:9\]\[tag:Column\] to \[id:11\]\[tag:NodeContainer\] with previous parent \[id:8\]\[tag:NodeContainer\]
    
2.  ArkUI Inspector工具可视化确认：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/1d/v3/exIzPOOHROyHRapAPWUuiw/zh-cn_image_0000002658926429.png?HW-CC-KV=V1&HW-CC-Date=20260709T133930Z&HW-CC-Expire=86400&HW-CC-Sign=AD78A19755603AEBB4246B2C5E682932A619A2BD990E982FB3AF16D13EB7D1E2)
    
    如图所示，通过工具可直观看到Column(9)同时被挂载NodeContainer(8)和NodeContainer(11)，形成节点的双挂现象。
    

#### 背景知识

*   [NodeContainer](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-nodecontainer)：作为容器节点存在，用于挂载自定义节点（如[FrameNode](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-framenode)或[BuilderNode](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-buildernode)），并通过[NodeController](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-nodecontroller)动态控制节点的挂载和卸载。
*   NodeController：管理自定义节点的生命周期，包括创建、显示、更新和销毁。NodeController通常搭配NodeContainer进行使用。一个NodeController只允许与一个NodeContainer进行绑定。此外，通过调用NodeController的[rebuild](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-nodecontroller#rebuild)方法通知NodeContainer组件重新回调[makeNode](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-nodecontroller#makenode)方法，更新子节点。

#### 解决方案

节点双挂问题的原因在于单个自定义节点（如BuilderNode）实例被多个NodeController引用并同时尝试挂载到不同的NodeContainer。这违反了每个节点只能有一个父节点的原则。

在实际开发中，开发者希望复用BuilderNode实例以提升性能，但存在下面问题：

*   单个BuilderNode实例不能同时作为多个NodeContainer的子节点。
*   创建多个BuilderNode实例虽可避免双挂，但会带来额外的内存开销和状态同步复杂度等等。

因此，更符合实际需求的解决方案是实现同一个BuilderNode实例在不同NodeContainer间的迁移，而非同时挂载。以下方案通过自定义NodeController实现节点挂载前的父节点状态检查，确保同一时间节点仅挂载到一个NodeContainer。整体的实现思路如下：

1.  创建共享的BuilderNode实例，为每个NodeContainer创建独立的NodeController，每个NodeController持有对共享BuilderNode的引用；
2.  在挂载节点前，检查节点是否已有父节点，确保节点未被挂载到其他父节点；
3.  通过detachContent/attachContent操作实现节点在不同容器间的迁移。

以下是示例代码：

1.  步骤一：创建自定义NodeController。
    
    import { BuilderNode, NodeController } from '@kit.ArkUI';
    
    _// 用于控制和反馈对应的NodeContainer上的节点的行为，需要与NodeContainer一起使用_
    export class MyNodeController extends NodeController {
      private builderNode: BuilderNode<\[string\]> | null | undefined = null;
      private rootNode: FrameNode | null = null;
    
      constructor(builderNode: BuilderNode<\[string\]> | undefined | null) {
        super();
        this.builderNode = builderNode;
      }
    
      makeNode(): FrameNode | null {
       _ // 返回要挂载到NodeContainer的根节点_
        return this.rootNode;
      }
    
     _ // 挂载Content_
      attachContent(): void {
        if (this.builderNode) {
          let frameNode: FrameNode | null = this.builderNode.getFrameNode();
         _ // 关键检查：确保节点未被挂载到其他父节点_
          if (frameNode?.getParent() != null) {
            return; _// 节点已有父节点，跳过_
          }
          this.rootNode = this.builderNode.getFrameNode();
        }
      }
    
     _ // 卸载Content_
      detachContent(): void {
        this.rootNode = null; _// 清空根节点引用_
      }
    }
    
    *   attachContent()方法在挂载前检查frameNode.getParent()，若已存在父节点则终止挂载。
    *   detachContent()清空rootNode，使节点可从当前NodeContainer安全卸载。
2.  步骤二：定义可复用的UI构建器。
    
    _// @Builder中为动态组件的具体组件内容_
    _// 这里只为举例说明，UI组件描述比较简单，实际应用场景中，这里是比较复杂的UI组件描述，如Web组件等_
    @Builder
    function ContentBuilder(data: string) {
      Column() {
        Text(data).fontSize(24);
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
      .backgroundColor(Color.Blue);
    }
    
    _// 包装Builder函数以供BuilderNode使用_
    let wrap = wrapBuilder<\[string\]>(ContentBuilder);
    
3.  步骤三：在入口组件实现节点的迁移。
    
    @Entry
    @Component
    struct Index {
      _// 单例BuilderNode，可被多个NodeController共享_
      private builderNode: BuilderNode<\[string\]> | null | undefined = new BuilderNode(this.getUIContext());
     _ // 两个NodeController共享同一BuilderNode实例_
      private nodeController1: MyNodeController = new MyNodeController(this.builderNode);
      private nodeController2: MyNodeController = new MyNodeController(this.builderNode);
    
      aboutToAppear(): void {
       _ // 初始化BuilderNode内容_
        this.builderNode?.build(wrap, 'This is a Text');
      }
    
      _// 本例两个不同的NodeContainer只一个组件内，只为举例演示直观_
      _// 实际应用场景中，NodeContainer通常在不同的页面，甚至不同的窗口中_
      build() {
        Column({ space: 20 }) {
        _  // 第一个NodeContainer区域_
          Column({ space: 20 }) {
            Button('Attach Content')
              .onClick(() => {
                this.nodeController1.attachContent();
                this.nodeController1.rebuild(); _// 触发界面更新_
              });
            Button('Detach Content')
              .onClick(() => {
                this.nodeController1.detachContent();
                this.nodeController1.rebuild();
              });
    
            NodeContainer(this.nodeController1)
              .height('50%')
              .width('50%')
              .borderWidth(1);
            Text('我是NodeContainer1');
          }
          .height('50%');
    
          Divider();
        _  // 第二个NodeContainer区域_
          Column({ space: 20 }) {
            Button('Attach Content')
              .onClick(() => {
                this.nodeController2.attachContent();
                this.nodeController2.rebuild();
              });
            Button('Detach Content')
              .onClick(() => {
                this.nodeController2.detachContent();
                this.nodeController2.rebuild();
              });
            NodeContainer(this.nodeController2)
              .height('50%')
              .width('50%')
              .borderWidth(1);
            Text('我是NodeContainer2');
          }
          .height('50%');
        }
        .height('100%');
      }
    }