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

92 阅读5分钟

前言

命令式弹窗能 inject 到父组件 provide 的数据,但它是没有父组件的——这看起来矛盾,实际上涉及 Vue 底层一套完整的影响链。

这篇文章从"为什么能 inject 到"这个问题出发,逐步拆解:

两种组件的区别
  → 命令式组件 parent = null
  → parent = null 导致 provides 初始化方式不同
  → provides 结构不同导致 inject 查找路径不同
  → 实际示例验证
  → 子组件的继承行为

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

什么是标准组件?

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

<!-- Parent.vue -->
<template>
  <!-- ✅ 标准组件:在模板中声明 -->
  <UserProfile />
</template>

特点:

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

什么是命令式组件?

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

// useCommandComponent.js
const initInstance = (Component, props, container, appContext) => {
    const vNode = createVNode(Component, props)
    vNode.appContext = appContext
    render(vNode, container)  // ← 手动渲染
    getAppendToElement(props).appendChild(container)
}
<!-- Parent.vue -->
<script setup>
const showProfile = useCommandComponent(UserProfile)

function open() {
  showProfile({ username: '张三' })  // ← 函数调用
}
</script>

特点:

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

两种组件最大的区别在于:标准组件由父组件的模板驱动渲染,命令式组件由开发者手动调用 render。这个区别直接决定了 parent 的值。


2. 为什么 parent = null?

标准组件的渲染流程

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

结果: UserProfile.parent = 父组件实例

命令式组件的渲染流程

// useCommandComponent 内部
render(vNode, container)

// Vue 内部
patch(null, vNode, container, null, ...)
//                          ^^^^
//                          parent 传的是 null

结果: UserProfile.parent = null

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

那么问题来了:parent = null 会影响什么?答案是 provides 的初始化方式


3. parent = null 的后果:provides 初始化不同

Vue 源码(简化版)

function createComponentInstance(vnode, parent, suspense) {
  const instance = {
    parent: parent,
    appContext: vnode.appContext,

    // 关键:provides 的初始化方式取决于 parent
    provides: parent
      ? parent.provides  // 有 parent → 直接引用父组件的 provides
      : Object.create(vnode.appContext.provides)  // 无 parent → 创建新对象,原型链指向 appContext
  }
  return instance
}

Vue 的逻辑很简单:有 parent 就复用父组件的 provides,没有就基于 appContext 创建一个新的

两种情况的内存结构

标准组件(有 parent)

父组件实例.provides = { currentUser: '张三' }

UserProfile实例.provides = 父组件实例.provides  // ← 同一个对象引用

父子共用同一个 provides 对象,所以子组件能直接访问父组件 provide 的数据。

命令式组件(parent = null)

appContext.provides = { currentUser: '张三' }

UserProfile实例.provides = {}  // 新空对象
UserProfile实例.provides.__proto__ → appContext.provides  // 原型链指向 appContext

provides 是独立的空对象,但通过原型链关联到了 appContext.provides

provides 的结构不同,直接影响了 provide 和 inject 的行为。


4. provides 结构如何影响 provide/inject

provide 的行为

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 的行为(核心差异)

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]
    }
  }
}

这就是命令式组件能 inject 到父组件数据的直接原因:

标准组件命令式组件
parent 值父组件实例null
inject 查找目标parent.providesappContext.provides

命令式组件的 inject 直接查 appContext.provides,而父组件 provide 的数据就存在这里,所以能找到。

理论讲完了,下面用一个实际场景走一遍完整流程。


5. 实际示例

场景

父组件提供了当前用户名,命令式弹窗需要 inject 拿到它来显示欢迎语。

<!-- Parent.vue -->
<script setup>
import { provide } from 'vue'

provide('currentUser', '张三')
const showProfile = useCommandComponent(UserProfile)
</script>
<!-- UserProfile.vue(命令式弹窗) -->
<script setup>
import { inject } from 'vue'

const user = inject('currentUser')
// 能拿到 '张三' 吗?
</script>

<template>
  <el-dialog :model-value="true" title="用户信息">
    <p>欢迎你,{{ user }}</p>
  </el-dialog>
</template>

执行流程

第1步:创建 UserProfile 实例

UserProfile实例.provides = {}
UserProfile实例.provides.__proto__ → appContext.provides = { currentUser: '张三' }

第2步:UserProfile setup 执行 inject

const user = inject('currentUser')
// parent === null → 查 appContext.provides
// appContext.provides.currentUser → ✅ 找到 '张三'

结果:user = '张三'

查找链路:

inject('currentUser')
  → parent === null
  → 查 appContext.provides
  → appContext.provides.currentUser = '张三'

那如果弹窗内部还有子组件呢?子组件的 inject 行为又是什么?


6. 子组件的情况

场景

UserProfile 弹窗内部有一个 UserAvatar 子组件,也需要访问当前用户名。

<!-- UserProfile.vue -->
<template>
  <el-dialog :model-value="true" title="用户信息">
    <p>欢迎你,{{ user }}</p>
    <UserAvatar />
  </el-dialog>
</template>

<script setup>
const user = inject('currentUser')
</script>
<!-- UserAvatar.vue -->
<template>
  <span>头像:{{ user }}</span>
</template>

<script setup>
const user = inject('currentUser')
</script>

UserAvatar 是标准组件(在 UserProfile 的模板中声明),所以:

// UserAvatar 实例
UserAvatar.parent = UserProfile实例
UserAvatar.provides = UserProfile实例.provides  // 初始时是同一个对象

如果 UserAvatar 也调用 provide

<!-- UserAvatar.vue -->
<script setup>
provide('avatarSize', 'large')
const user = inject('currentUser')
</script>

provide 内部检测到 provides === parent.provides(还是同一个引用),触发写时复制:

// 写时复制:创建新对象
UserAvatar.provides = Object.create(UserProfile实例.provides)
UserAvatar.provides.__proto__UserProfile实例.provides

// 写入自己的 provides
UserAvatar.provides.avatarSize = 'large'

UserAvatar 的 inject 查找链

const user = inject('currentUser')
// parent !== null → 查 parent.provides(即 UserProfile 的 provides)
// UserProfile.provides 没有 currentUser
// 沿原型链 → appContext.provides.currentUser → ✅ 找到 '张三'

总结子组件的行为:

  • 不调用 provide → provides 就是父组件的 provides(同一个引用)
  • 调用 provide → 写时复制,创建新对象,原型链指向父组件的 provides
  • inject 时 → 查 parent.provides,找不到则沿原型链向上找

7. 小结

回到开头的问题:命令式组件为什么能 inject 到父组件的 provide 数据?

完整的因果链:

命令式组件通过 render 直接挂载
  → Vue 将其视为根组件,parent = null
  → parent = null → provides 初始化为 Object.create(appContext.provides)
  → inject 检测到 parent == null → 直接查 appContext.provides
  → 父组件的 provide 数据存在 appContext.provides 中
  → 所以能 inject 到 ✅

核心要点:

  • parent = null:命令式组件是直接 render 挂载的,没有父组件
  • provides 初始化:用 Object.create(appContext.provides) 创建独立对象
  • inject 查找路径:命令式组件查 appContext.provides,标准组件查 parent.provides
  • 写时复制:provide 时如果 provides 还和父组件共用,会先创建新对象再写入

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

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

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