破解Vue自定义弹窗销毁的"nextSibling"之谜
问题场景描述
在动态创建的Dialog组件内部再次使用Dialog时,触发Cannot read properties of null (reading 'nextSibling')错误。
最小复现代码
1.弹窗前端组件
            
            
              html
              
              
            
          
          <!-- ParentDialog.vue -->
<template>
  <!-- 父弹窗直接使用el-dialog -->
  <el-dialog v-model="parentVisible" title="父弹窗">
    <button @click="openChild">打开子弹窗</button>
    <!-- 子弹窗组件 -->
    <el-dialog v-model="childVisible" title="子弹窗" width="60%" append-to-body>
      <p>我是嵌套的子弹窗内容</p>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
    import { ref } from 'vue'
    const parentVisible = ref(true) 
    const childVisible = ref(false) 
    const openChild = () => { childVisible.value = true }
</script>
        2.调用代码
            
            
              typescript
              
              
            
          
          import ParentDialog from "@component/ParentDialog.vue"
  function showDialog(){
  createDialog(ParentDialog);
}
        3.createDialog函数的代码
            
            
              typescript
              
              
            
          
          import ElementUi from 'element-plus';
export const createDialog = (
  Node: Component<any, any, any, ComputedOptions, MethodOptions, {}, any>
) => {
  const div = document.createElement('div');
  div.id = 'test-dialog';
  document.body.appendChild(div);
  const app = createApp(Node, {
    onClose() {
      console.log('弹窗关闭了');
      app.unmount();
      div.remove();
    }
  });
  app.use(ElementUi);
  app.mount('#test-dialog');
};
        问题出在了哪呢?
问题主要来源于 Vue 组件生命周期与 DOM 操作的冲突。在嵌套使用 el-dialog 时:
- 动态创建和销毁组件 :父弹窗和子弹窗分别以动态方式挂载到 DOM 上,子弹窗由于 
append-to-body被移到body下,这使得 Vue 的虚拟 DOM 与真实 DOM 不完全同步。 nextSibling属性访问 :Element-Plus在处理某些 DOM 操作时,依赖父子元素的 DOM 顺序,但动态挂载会导致 DOM 层级被打乱,从而触发错误。
问题如图所示
sequenceDiagram
    participant App as Vue App
    participant Parent as Parent Dialog
    participant Child as Child Dialog
    participant DOM as Real DOM
    App->>+Parent: 1. 创建父弹窗组件
    Parent->>+DOM: 2. 挂载到 DOM
    Parent->>+Child: 3. 动态创建子弹窗
    Child->>+DOM: 4. 使用 append-to-body 挂载到 body
    Note over DOM: 子弹窗被移到 body 下
    Parent->>Child: 5. 操作子弹窗状态
    Child->>DOM: 6. 更新挂载位置
    DOM->>DOM: 7. 尝试 nextSibling 获取
    DOM-->>-App: 抛出错误
"Cannot read properties of null" deactivate DOM deactivate Child deactivate Parent
"Cannot read properties of null" deactivate DOM deactivate Child deactivate Parent
解决办法详解
- 使用 
teleport明确挂载位置
在弹窗嵌套场景下,通过teleport将子弹窗直接挂载到body下,这样可以避免 Vue 在嵌套管理 DOM 时的复杂性,保持组件逻辑清晰。 - 调整 
append-to-body和挂载点
在子弹窗的el-dialog上启用append-to-body,但需确保它的生命周期与父组件一致。 - 事件冒泡和通信优化
使用 Vue 的emit或者自定义事件来控制子弹窗的打开和关闭状态,而不是直接操作 DOM。这样可以减少对nextSibling的依赖。 
解决方案如图所示
sequenceDiagram
    participant App as Vue App
    participant Parent as Parent Dialog
    participant Child as Child Dialog
    participant DOM as Real DOM
    App->>+Parent: 1. 创建父弹窗组件
    Parent->>+DOM: 2. 挂载到 DOM
    Parent->>+Child: 3. 使用 Teleport 明确挂载位置
    Child->>+DOM: 4. 挂载到 body
    Parent->>Child: 5. 操作子弹窗状态
    Child->>DOM: 6. Teleport 管理挂载
    DOM-->>-App: 7. 正常完成所有操作
    deactivate Child
    deactivate Parent
修改后的代码
            
            
              html
              
              
            
          
          <template>
  <!-- 父弹窗 -->
  <el-dialog v-model="dialogState.parentVisible" title="父弹窗" @close="emitClose">
    <button @click="openChild">打开子弹窗</button>
    <!-- 子弹窗通过 teleport 挂载到 body -->
    <teleport to="body">
      <el-dialog
        v-model="dialogState.childVisible"
        title="子弹窗"
        width="60%"
        @close="handleChildClose"
      >
        <p>我是嵌套的子弹窗内容</p>
      </el-dialog>
    </teleport>
  </el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, defineProps, defineEmits } from 'vue';
// 定义 props 和 emits
const props = defineProps({
  onClose: {
    type: Function,
    default: null,
  },
});
const emit = defineEmits(['close']);
// 控制弹窗状态
const dialogState = reactive({
  parentVisible: true,
  childVisible: false,
});
// 定义 expose 方法供父组件调用
defineExpose({
  close: () => {
    dialogState.parentVisible = false;
  },
});
// 打开子弹窗
const openChild = () => {
  dialogState.childVisible = true;
};
// 子弹窗关闭逻辑
const handleChildClose = () => {
  dialogState.childVisible = false;
};
// 父弹窗关闭时触发回调
const emitClose = () => {
  if (typeof props.onClose === 'function') {
    props.onClose();
  }
  emit('close');
};
</script>
        描述 :仅仅改动了 子弹窗的挂载方式,将其通过 teleport 明确指定到 body ,从而避免 DOM 层级错乱的问题,而 createDialog 函数的代码不需要修改哦~
总结
其实只需要在组件中使用 teleport 明确指定子弹窗的挂载位置,同时让父子弹窗的状态管理逻辑清晰即可啦~