破解Vue自定义弹窗销毁的"nextSibling"之谜

破解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 时:

  1. 动态创建和销毁组件 :父弹窗和子弹窗分别以动态方式挂载到 DOM 上,子弹窗由于 append-to-body 被移到 body 下,这使得 Vue 的虚拟 DOM 与真实 DOM 不完全同步。
  2. 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: 抛出错误<br>"Cannot read properties of null" deactivate DOM deactivate Child deactivate Parent

解决办法详解

  1. 使用 teleport 明确挂载位置
    在弹窗嵌套场景下,通过 teleport 将子弹窗直接挂载到 body 下,这样可以避免 Vue 在嵌套管理 DOM 时的复杂性,保持组件逻辑清晰。
  2. 调整 append-to-body 和挂载点
    在子弹窗的 el-dialog 上启用 append-to-body,但需确保它的生命周期与父组件一致。
  3. 事件冒泡和通信优化
    使用 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 明确指定子弹窗的挂载位置,同时让父子弹窗的状态管理逻辑清晰即可啦~

相关推荐
东方小月19 分钟前
从零开发一个 Coding Agent(七):实现纯文本 Agent Loop
前端·人工智能
大家的林语冰20 分钟前
👉 尤雨溪再次成立新公司,同时官宣 Pinia 4 正式发布!
前端·javascript·vue.js
浮生望23 分钟前
React useRef 深度解析:从DOM操作到Web Worker多线程协作
前端
浮生望29 分钟前
React Router v7 实战全景:路由配置、嵌套路由、鉴权守卫与懒加载完整指南
前端
用户9385156350731 分钟前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战(二)
前端·javascript·typescript
雪隐1 小时前
个人电脑玩AI-16让5060 Ti给你打工——MiniMax H3 提速实录:从"泡杯茶等视频"到"视频等你",一张 5060 Ti 的自我修养
前端·人工智能·后端
FungLeo1 小时前
Flutter Web 中文字体困境与渲染器选择:CanvasKit vs HTML renderer 实战
前端·flutter·html
敲代码的玉米C2 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
gis开发之家2 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
Narrastory2 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github