📄 第二篇: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 污染与关闭动画修复

相关推荐
iReachers2 小时前
HTML打包EXE工具数据加密功能详解 - 加密保护HTML/JS/CSS资源
javascript·css·html·html加密·html转exe·html一键打包exe·exe打包
kyriewen112 小时前
代码写成一锅粥?这5种设计模式让你的项目“起死回生”
前端·javascript·设计模式·typescript·ecmascript·html5
ywlovecjy2 小时前
【Nginx 】Nginx 部署前端 vue 项目
前端·vue.js·nginx
skywalk81632 小时前
g4f JavaScript调用报错问题解决
开发语言·javascript·ecmascript
Alice-YUE2 小时前
AI对话为什么需要RAG
前端·语言模型·rag
C澒2 小时前
IntelliPro 企业级产研协作平台:低代码实时预览与可视化编辑技术调研
前端·低代码·ai编程
霍理迪2 小时前
TS类型断言和类型守卫
前端
木斯佳2 小时前
前端八股文面经大全:京东前端实习一面(2026-04-16)·面经深度解析
前端
chenxu98b2 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端