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

1. 标准组件 vs 命令式组件

什么是标准组件?

通过模板声明,由 Vue 自动管理。

xml 复制代码
<!-- App.vue -->
<template>
  <!-- ✅ 标准组件:在模板中声明 -->
  <ChildComponent />
</template>

特点:

  • 写在 <template>
  • parent 指向父组件实例

什么是命令式组件?

通过函数调用创建,手动挂载到 DOM。

javascript 复制代码
// useCommandComponent.js
export const useCommandComponent = (Component) => {
  const container = document.createElement('div')
  
  return (options = {}) => {
    const vNode = createVNode(Component, options)
    render(vNode, container)  // ← 手动渲染
    document.body.appendChild(container)
  }
}
xml 复制代码
<!-- App.vue - 使用 -->
<script setup>
const showModal = useCommandComponent(TestModal)
​
function open() {
  showModal({ title: '弹窗' })  // ← 函数调用
}
</script>

特点:

  • 不在模板中声明
  • 通过函数调用(如 showModal()
  • parent = null(没有父组件)

2. 为什么 parent = null?

标准组件的渲染流程

scss 复制代码
// Vue 内部
patch(parentVNode, childVNode, container, parentComponent)
//                                        ^^^^^^^^^^^^^^
//                                        传入父组件实例

结果: ChildComponent.parent = App实例


命令式组件的渲染流程

scss 复制代码
// useCommandComponent 内部
render(vNode, container)
​
// Vue 内部
patch(null, vNode, container, null, ...)
//                          ^^^^
//                          parent 传的是 null

结果: TestModal.parent = null

原因: 命令式组件不是通过父组件模板渲染的,而是直接 render 到 DOM,Vue 将其视为"根组件"。


3. provides 初始化逻辑

Vue 源码(简化版)

javascript 复制代码
function createComponentInstance(vnode, parent, suspense) {
  const instance = {
    parent: parent,
    appContext: vnode.appContext,
    
    // 关键:provides 的初始化方式
    provides: parent 
      ? parent.provides  // 有 parent:直接引用父组件的 provides
      : Object.create(vnode.appContext.provides)  // 无 parent:创建新对象
  }
  return instance
}

两种情况的内存结构

标准组件(ChildComponent 的父组件是 App)

ini 复制代码
App实例.provides = { config: 'app数据' }
​
ChildComponent实例.provides = App实例.provides  // ← 同一个对象引用

特点: 父子共用同一个 provides 对象。


命令式组件(TestModal 在 App 中创建)

ini 复制代码
appContext.provides = { config: 'app数据' }
​
TestModal实例.provides = {}  // 新空对象
TestModal实例.provides.__proto__ → appContext.provides

特点:

  • provides 是独立空对象
  • 原型链指向 appContext.provides

4. provide/inject 的逻辑

provide 的行为

scss 复制代码
function provide(key, value) {
  const instance = getCurrentInstance()
  
  // 如果 provides 和 parent.provides 是同一个对象
  if (instance.parent && instance.provides === instance.parent.provides) {
    // 写时复制:创建新对象,避免污染父组件
    instance.provides = Object.create(instance.provides)
  }
  
  // 写入自己的 provides
  instance.provides[key] = value
}

关键点: provide 总是写入当前实例自己的 provides


inject 的行为(核心差异)

javascript 复制代码
function inject(key) {
  const instance = getCurrentInstance()
  
  if (instance.parent == null) {
    // ⚠️ 命令式组件走这里
    const provides = instance.vnode.appContext.provides
    // 查的是 appContext.provides,不是 instance.provides
    if (key in provides) {
      return provides[key]
    }
  } else {
    // 标准组件走这里
    const provides = instance.parent.provides
    // 查的是父组件的 provides
    if (key in provides) {
      return provides[key]
    }
  }
}

关键差异:

  • 标准组件injectparent.provides
  • 命令式组件injectappContext.provides

5. 实际示例

场景设置

php 复制代码
// App.vue
provide('config', { theme: 'dark' })
const showModal = useCommandComponent(TestModal)
xml 复制代码
<!-- TestModal.vue -->
<script setup>
provide('modalConfig', { title: '我是弹窗' })
const config = inject('config')  // ← 能拿到吗?
</script>

执行流程

1. 创建 TestModal 实例

ini 复制代码
TestModal实例.provides = {}
TestModal实例.provides.__proto__ → appContext.provides = { config: { theme: 'dark' } }

2. TestModal setup 执行

arduino 复制代码
// provide
provide('modalConfig', { title: '我是弹窗' })
// TestModal实例.provides = { modalConfig: { title: '我是弹窗' } }
​
// inject
const config = inject('config')
// 因为 parent === null
// 查的是 appContext.provides
// appContext.provides.config → ✅ 找到 { theme: 'dark' }

结果: config = { theme: 'dark' }


6. 子组件的情况

ChildTestModal(TestModal 的子组件)

xml 复制代码
<!-- TestModal.vue 模板 -->
<template>
  <ChildTestModal />
</template>
ini 复制代码
// ChildTestModal 实例
ChildTestModal.parent = TestModal实例
ChildTestModal.provides = TestModal实例.provides  // 初始时是同一个对象

ChildTestModal 调用 provide

xml 复制代码
<!-- ChildTestModal.vue -->
<script setup>
provide('childData', '子组件数据')
</script>
ini 复制代码
// provide 内部检测到 provides === parent.provides
ChildTestModal.provides = Object.create(TestModal实例.provides)
// 现在是一个新对象
ChildTestModal.provides.__proto__ → TestModal实例.provides
​
ChildTestModal.provides.childData = '子组件数据'

结果: 子组件不调用 provide 函数他的 provides 就等于父组件的 provides, 调用 provide 函数子组件的 provides 就是一个原型链指向父组件 provides 的新对象。


7. 小结

核心要点

  1. 命令式组件 parent = null :因为是直接 render 挂载,没有父组件
  2. provides 初始化不同 :命令式组件用 Object.create 创建独立对象
  3. inject 查找链不同 :命令式组件查 appContext.provides,标准组件查 parent.provides
  4. 子组件有无 provide 时结果不同 :避免污染父组件的 provides

所以这是命令式组件可以 inject 到 App provide 提供的数据的原理 ✅

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

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

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

相关推荐
IT_陈寒12 分钟前
Vue的动态组件坑了我整整一天!
前端·人工智能·后端
恋猫de小郭14 分钟前
Flutter 最好的 AI 自动化测试工具:Patrol
android·前端·flutter
Cobyte16 分钟前
AI 的个人便签纸:Claude Code 的 TodoWrite 模式
前端·后端·aigc
森叶18 分钟前
一线法编程理念
javascript
风兮雨露23 分钟前
Java 从入门到精通,前端资料
java·开发语言·前端
ZC跨境爬虫28 分钟前
跟着 MDN 学CSS day_43:CSS布局挑战——从浮动到弹性盒与栅格的综合实践
前端·css·ui·html·tensorflow
源码宝28 分钟前
基于SpringBoot+Vue+小程序+Android的智慧校园电子班牌系统源码示例
vue.js·spring boot·架构·智慧校园·电子班牌·源码·代码
罗超驿34 分钟前
22.任务清单应用开发实战:从HTML结构到JavaScript交互的完整实现
javascript·html·交互
Qres82136 分钟前
Hexo博客本地配置
前端·博客·hexo
Bigger38 分钟前
GitLab-Runner + AI 代码审查服务 + 远程大模型 全套部署运维实战
前端·ci/cd·ai编程