1. 标准组件 vs 命令式组件
什么是标准组件?
通过模板声明,由 Vue 自动管理。
<!-- App.vue -->
<template>
<!-- ✅ 标准组件:在模板中声明 -->
<ChildComponent />
</template>
特点:
- 写在
<template>里 parent指向父组件实例
什么是命令式组件?
通过函数调用创建,手动挂载到 DOM。
// 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)
}
}
<!-- App.vue - 使用 -->
<script setup>
const showModal = useCommandComponent(TestModal)
function open() {
showModal({ title: '弹窗' }) // ← 函数调用
}
</script>
特点:
- 不在模板中声明
- 通过函数调用(如
showModal()) parent = null(没有父组件)
2. 为什么 parent = null?
标准组件的渲染流程
// Vue 内部
patch(parentVNode, childVNode, container, parentComponent)
// ^^^^^^^^^^^^^^
// 传入父组件实例
结果: ChildComponent.parent = App实例
命令式组件的渲染流程
// useCommandComponent 内部
render(vNode, container)
// Vue 内部
patch(null, vNode, container, null, ...)
// ^^^^
// parent 传的是 null
结果: TestModal.parent = null
原因: 命令式组件不是通过父组件模板渲染的,而是直接 render 到 DOM,Vue 将其视为"根组件"。
3. provides 初始化逻辑
Vue 源码(简化版)
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)
App实例.provides = { config: 'app数据' }
ChildComponent实例.provides = App实例.provides // ← 同一个对象引用
特点: 父子共用同一个 provides 对象。
命令式组件(TestModal 在 App 中创建)
appContext.provides = { config: 'app数据' }
TestModal实例.provides = {} // 新空对象
TestModal实例.provides.__proto__ → appContext.provides
特点:
provides是独立空对象- 原型链指向
appContext.provides
4. 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.provides - 命令式组件:
inject查appContext.provides
5. 实际示例
场景设置
// App.vue
provide('config', { theme: 'dark' })
const showModal = useCommandComponent(TestModal)
<!-- TestModal.vue -->
<script setup>
provide('modalConfig', { title: '我是弹窗' })
const config = inject('config') // ← 能拿到吗?
</script>
执行流程
1. 创建 TestModal 实例
TestModal实例.provides = {}
TestModal实例.provides.__proto__ → appContext.provides = { config: { theme: 'dark' } }
2. TestModal setup 执行
// 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 的子组件)
<!-- TestModal.vue 模板 -->
<template>
<ChildTestModal />
</template>
// ChildTestModal 实例
ChildTestModal.parent = TestModal实例
ChildTestModal.provides = TestModal实例.provides // 初始时是同一个对象
ChildTestModal 调用 provide
<!-- ChildTestModal.vue -->
<script setup>
provide('childData', '子组件数据')
</script>
// 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. 小结
核心要点
- 命令式组件
parent = null:因为是直接render挂载,没有父组件 - provides 初始化不同:命令式组件用
Object.create创建独立对象 - inject 查找链不同:命令式组件查
appContext.provides,标准组件查parent.provides - 子组件有无 provide 时结果不同:避免污染父组件的
provides
所以这是命令式组件可以 inject 到 App provide 提供的数据的原理 ✅