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

#### 问题现象

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

1.  hilog日志确认：
    
    ```
    Add [id:9][tag:Column] to [id:11][tag:NodeContainer] with previous parent [id:8][tag:NodeContainer]
    ```
    
2.  ArkUI Inspector工具可视化确认：
    
    如图所示，通过工具可直观看到Column(9)同时被挂载NodeContainer(8)和NodeContainer(11)，形成节点的双挂现象。
    

#### 背景知识

*   NodeContainer：作为容器节点存在，用于挂载自定义节点（如FrameNode或BuilderNode），并通过NodeController动态控制节点的挂载和卸载。
*   NodeController：管理自定义节点的生命周期，包括创建、显示、更新和销毁。NodeController通常搭配NodeContainer进行使用。一个NodeController只允许与一个NodeContainer进行绑定。此外，通过调用NodeController的rebuild方法通知NodeContainer组件重新回调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%');
      }
    }
    ```