📄 第三篇:Vue 3 命令式弹窗 Provide 污染与关闭动画修复

问题背景

在使用 useCommandComponent 封装命令式弹窗时,遇到了两个典型的底层机制问题:

  1. Provide/Inject 数据污染:多次打开弹窗后,子组件 inject 到了上一次残留的旧数据。
  2. 关闭动画丢失:弹窗关闭时 DOM 被过早销毁,导致过渡动画无法完整播放。

修复点1:AppContext 上下文污染

1. 问题复现

测试场景

xml 复制代码
<!-- TestModal.vue -->
<script setup>
import { provide, inject } from 'vue'
​
// 每次打开都 provide 一个随机值
provide('modalConfig', Math.random())
​
// 尝试 inject 同一个 key
const config = inject('modalConfig')
console.log('inject 结果:', config)
</script>
scss 复制代码
// App.vue
const showModal = useCommandComponent(TestModal)

操作流程与现象

操作 provide 的值 inject 预期 inject 实际 状态
第1次打开 0.123456 undefined undefined ✅ 正常
关闭弹窗 - - - -
第2次打开 0.789012 undefined 0.123456 ❌ 污染

关键特征:

  • 首次运行正常,第2次才出问题。
  • 不报错,只是数据不对(最难排查的 bug 类型)。
  • 拿到的不是本次 provide 的值,而是上一次的残留。

2. 根本原因分析

错误代码

javascript 复制代码
// useCommandComponent.js - 有问题的版本
export const useCommandComponent = (Component) => {
  const instance = getCurrentInstance()
  
  // ❌ 直接修改全局 appContext.provides
  const appContext = instance?.appContext
  const currentProvides = instance?.provides
  
  if (appContext && currentProvides) {
    Reflect.set(appContext, 'provides', currentProvides)
  }
  
  // ...
}

污染链路详解

初始状态:

ini 复制代码
App.appContext.provides = {}

第1次注册(App.vue 中调用):

c 复制代码
const showModal = useCommandComponent(TestModal)
​
// instance = App 实例
// currentProvides = App.provides = {}
​
// 覆盖全局(此时是空对象,暂时没问题)
Reflect.set(App.appContext, 'provides', {})

第1次打开弹窗:

scss 复制代码
showModal()
​
// 创建 TestModal 实例1
TestModal实例1.provides = Object.create(App.appContext.provides)
​
// setup 执行
provide('modalConfig', 0.123456)
// TestModal实例1.provides = { modalConfig: 0.123456 }
​
const config = inject('modalConfig')
// 查询链:TestModal实例1.provides → App.appContext.provides
// 结果:undefined ✅(符合预期)
​
// ⚠️ 如果内部嵌套调用 useCommandComponent
const showChild = useCommandComponent(ChildComponent)
// currentProvides = TestModal实例1.provides
// Reflect.set(App.appContext, 'provides', TestModal实例1.provides)
// App.appContext.provides = { modalConfig: 0.123456 } ❌ 被污染!

关闭弹窗:

arduino 复制代码
// unmount(TestModal实例1)
​
// ❌ 但 App.appContext.provides 没有被恢复
// 仍然是:{ modalConfig: 0.123456 }

第2次打开弹窗:

scss 复制代码
showModal()
​
// 创建 TestModal 实例2
TestModal实例2.provides = Object.create(App.appContext.provides)
// TestModal实例2.provides.__proto__ = { modalConfig: 0.123456 } ❌ 原型链指向旧数据
​
// setup 执行
provide('modalConfig', 0.789012)
// TestModal实例2.provides = { modalConfig: 0.789012 }
​
const config = inject('modalConfig')
// 查询链:TestModal实例2.provides → App.appContext.provides
// 结果:0.123456 ❌ 拿到了第1次的残留数据!

核心问题

复制代码
嵌套调用 useCommandComponent 
  ↓
覆盖全局 appContext.provides
  ↓
关闭后未恢复
  ↓
新实例的原型链指向旧数据
  ↓
inject 通过原型链查到残留值

3. 修复方案

修复代码

javascript 复制代码
// useCommandComponent.js - 修复版本(第30-35行)
export const useCommandComponent = (Component) => {
  const instance = getCurrentInstance()
  
  // ✅ 先复制 appContext,再独立设置 provides
  const appContext = { ...instance?.appContext }
  Reflect.set(appContext, 'provides', currentProvides)
  
  // ...
}

修复原理对比

修复前(有问题):

c 复制代码
const appContext = instance?.appContext
Reflect.set(appContext, 'provides', currentProvides)
// ❌ 所有调用共用同一个 appContext,互相覆盖

修复后(正确):

c 复制代码
const appContext = { ...instance?.appContext }
Reflect.set(appContext, 'provides', currentProvides)
// ✅ 两步操作:
//    1. 创建完全独立的新对象
//    2. 单独设置 provides 属性
//    彻底隔离,互不影响

关键点:

  • { ...instance?.appContext } 创建全新的独立对象。
  • 新对象与原始 appContext 没有任何关联。

修复点2:onClosed 回调与关闭动画

1. Element Plus 的关闭事件机制

Element Plus 的 <el-dialog> 有两个关闭相关的事件:

事件 触发时机 说明
@close 用户点击关闭按钮时 动画开始前立即触发
@closed 关闭动画播放完成后 动画结束后触发

完整执行流程:

less 复制代码
用户点击关闭按钮
  ↓
① @close 触发(此时动画还没开始)
  ↓
② 播放关闭动画(约 300ms)
  ↓
③ @closed 触发(动画已完全结束)

2. 为什么要用 onClosed 而不是 onClose?

关键问题:DOM 清理时机

命令式弹窗需要在关闭后清理 DOM:

scss 复制代码
const closed = () => {
  render(null, container)        // 卸载组件
  container.remove()             // 移除 DOM
}

如果在 @close 时清理:

less 复制代码
用户点击关闭
  ↓
@close 触发
  ↓
立即执行 closed() → render(null, container)
  ↓
❌ 组件被销毁,动画无法继续播放
❌ 用户看到弹窗"瞬间消失",没有过渡效果

如果在 @closed 时清理:

less 复制代码
用户点击关闭
  ↓
@close 触发(动画开始)
  ↓
播放关闭动画(300ms)✅ 动画完整播放
  ↓
@closed 触发(动画结束)
  ↓
执行 closed() → render(null, container) ✅ 安全清理

3. Vue 属性透传的作用

当弹窗组件作为根元素且未在 defineProps 中声明 onClosed 时,Vue 会自动将其透传给内部的 <el-dialog>

xml 复制代码
<!-- TestModal.vue -->
<template>
  <!-- el-dialog 是根元素 -->
  <el-dialog v-model="visible">
    弹窗内容
  </el-dialog>
</template>
​
<script setup>
// 没有声明 onClosed,Vue 自动透传
</script>

外部调用:

css 复制代码
showModal({
  onClosed: () => console.log('用户回调')
})

结果: onClosed 被透传给 <el-dialog>,相当于:

ini 复制代码
<el-dialog v-model="visible" @closed="onClosed">

4. 实现方案

核心逻辑

javascript 复制代码
// useCommandComponent.js(第42-60行)
​
// 清理函数
const closed = () => {
  render(null, container)
  container.parentNode?.removeChild(container)
}
​
const CommandComponent = (options = {}) => {
  // ... 其他逻辑
  
  // ✅ 统一处理 onClosed,确保动画完整 + DOM 清理
  if (typeof options.onClosed !== 'function') {
    // 用户没提供 onClosed,使用默认清理函数
    options.onClosed = closed
  } else {
    // 用户提供了 onClosed,包裹一层确保能清理 DOM
    const originOnClosed = options.onClosed
    options.onClosed = (...args) => {
      originOnClosed(...args)  // 先执行用户回调
      closed()                 // 再执行 DOM 清理
    }
  }
  
  // ...
}

📄 第一篇:Vue 3 命令式弹窗使用指南

📄 第二篇:Vue 3 命令式弹窗 provide/inject 机制解析

📄 第三篇:Vue 3 命令式弹窗 Provide 污染与关闭动画修复

相关推荐
问心无愧051317 小时前
ctf show web入门27
前端
小村儿17 小时前
给 AI Agent 装上"长期记忆":Karpathy 的 LLM Wiki 思想,我做成了工具
前端·后端·ai编程
竹林81817 小时前
用ethers.js连接MetaMask实现Web3钱包登录:从踩坑到稳定运行的完整记录
前端·javascript
heyCHEEMS17 小时前
如何用 Recast 实现静态配置文件源码级读写
前端·node.js
心连欣17 小时前
从零开始,学习所有指令!
前端·javascript·vue.js
review4454317 小时前
大模型和function calling分别是如何工作的
前端
东东同学17 小时前
耗时一个月,我把 Nuxt 首屏性能排障经验做成了一个 AI Skill
前端·agent
冴羽18 小时前
超越 Vibe Coding —— AI 辅助编程指南
前端·ai编程·vibecoding
梦想的颜色18 小时前
一天一个SKILL——前端最佳自动化测试 webapp-testing
前端·web app
SoaringHeart19 小时前
Flutter进阶:放弃 MediaQuery.of(context) 使用 NScreenManager
前端·flutter