Vue 3 命令式弹窗组件 这篇文章是 Vue3 命令式弹窗的实现,本文针对实现进行原理讲解。
核心问题
问题 :通过命令式方法(如render函数)创建多个弹窗组件实例时,为什么每个实例调用provide()时数据不会互相污染?
关键代码与机制
1. 创建命令式弹窗的核心代码
javascript
export const useCommandComponent = (Component) => {
const parentInstance = getCurrentInstance()
// 创建新的 appContext,继承父级上下文
const appContext = Object.create(parentInstance?.appContext)
// 关键步骤:设置 appContext.provides 为父级的 provides
if (appContext) {
Reflect.set(appContext, 'provides', parentInstance?.provides)
}
const container = document.createElement('div')
const CommandComponent = (options = {}) => {
const vNode = createVNode(Component, options)
vNode.appContext = appContext // 我们设置的 appContext
// 注意:render 函数没有传入 parentComponent
// 这意味着命令式组件被当作"根组件"处理
render(vNode, container)
document.body.appendChild(container)
return vNode
}
return CommandComponent
}
2. Vue 内部渲染逻辑
csharp
// Vue 内部的 render 函数简化逻辑
const render = (vnode, container) => {
// 第5个参数是 parentComponent,对于命令式组件,这里传的是 null
patch(null, vnode, container, null, null, null, namespace)
// ↑
// 这个 null 就是 parent,意味着命令式组件没有父组件
}
3. 组件实例创建的关键逻辑
javascript
// Vue 源码:createComponentInstance
function createComponentInstance(vnode, parent, suspense) {
const instance = {
// 关键:命令式组件的 parent 是 null!
parent: parent, // 对于命令式组件,parent = null
appContext: vnode.appContext, // 我们设置的 appContext
// 最关键的部分:provides 的初始化方式
provides: parent
? parent.provides // 有 parent 时,直接继承(标准组件)
: Object.create(vnode.appContext.provides) // 无 parent 时,创建新对象
// ↑
// 命令式组件走到这个分支
// 创建一个以 vnode.appContext.provides 为原型的新空对象
}
return instance
}
为什么不会互相污染?
1. 实例创建过程
javascript
// 创建弹窗1时
const instance1 = createComponentInstance(vNode1, parent = null, suspense)
// 因为 parent = null,所以:
instance1.provides = Object.create(vnode.appContext.provides)
// 结果:instance1.provides 是一个新的空对象 {}
// 但这个对象的 __proto__ 指向 vnode.appContext.provides(即父组件的 provides)
// 创建弹窗2时
const instance2 = createComponentInstance(vNode2, parent = null, suspense)
// 同样因为 parent = null:
instance2.provides = Object.create(vnode.appContext.provides)
// 结果:instance2.provides 是另一个新的空对象 {}
// 注意:instance1.provides 和 instance2.provides 是两个不同的对象
2. 实例状态对比
csharp
// 弹窗1实例的状态
instance1 = {
parent: null, // 没有父组件
provides: {}, // 全新的空对象1
// 关键:这个空对象的原型指向父组件的 provides
// provides.__proto__ === 父组件的provides
}
// 弹窗2实例的状态
instance2 = {
parent: null, // 同样没有父组件
provides: {}, // 全新的空对象2
// 注意:instance2.provides 和 instance1.provides 是不同的对象!
// 但它们的原型都指向同一个父组件的 provides
// provides.__proto__ === 父组件的provides
}
3. 调用 provide 时的行为
arduino
// 弹窗1调用 provide
provide('key1', 'value1')
// 实际执行:instance1.provides.key1 = 'value1'
// 结果:instance1.provides = { key1: 'value1' }
// 弹窗2调用 provide
provide('key2', 'value2')
// 实际执行:instance2.provides.key2 = 'value2'
// 结果:instance2.provides = { key2: 'value2' }
// 重要:这两个操作完全独立
// instance1.provides 和 instance2.provides 是两个不同的对象
// 所以不会互相影响
内存结构可视化
css
父组件
│
├── provides: { parentKey: 'parentValue' }
│
├── 弹窗1实例
│ ├── parent: null
│ ├── provides: { key1: 'value1' } ← 这是自有属性
│ │
│ └── provides.__proto__
│ ↓
│ { parentKey: 'parentValue' } ← 父组件的 provides
│
└── 弹窗2实例
├── parent: null
├── provides: { key2: 'value2' } ← 这是自有属性
│
└── provides.__proto__
↓
{ parentKey: 'parentValue' } ← 父组件的 provides
关键点总结
1. 为什么 parent 是 null?
- 命令式组件不是通过父组件模板渲染的
- 而是通过
render()函数直接挂载到 DOM - Vue 内部将其视为独立的"根组件"
- 所以
parent参数为null
2. 为什么 provides 是独立的对象?
-
当
parent为null时,Vue 会执行:vbnetprovides: Object.create(vnode.appContext.provides) -
这创建了一个新的空对象,其原型指向父组件的 provides
-
每个命令式组件实例都会执行这个操作
-
所以每个实例都有自己独立的
provides对象
3. 如何实现数据共享?
-
虽然每个实例的
provides是独立的对象 -
但这些对象的原型都指向同一个父组件的
provides -
当调用
inject()查找数据时:javascript// 简化版的 inject 逻辑 function inject(key) { // 对于命令式组件,instance.parent 为 null const provides = instance.parent == null // 走这个分支,刚好 appContext.provides 就是父组件的 provides ? instance.vnode.appContext.provides : instance.parent.provides if (provides && key in provides) { return provides[key] } } -
所以所有命令式组件都能访问父组件提供的数据
4. 为什么不会互相污染?
- 每个实例的
provides是不同的对象 - 调用
provide()时,数据写入各自实例的provides对象 - 实例A写入的数据在实例A的
provides对象上 - 实例B写入的数据在实例B的
provides对象上 - 它们之间没有直接联系,所以不会互相影响
实际示例
php
// 父组件提供配置
provide('appConfig', { theme: 'dark', version: '1.0' })
// 创建命令式弹窗
const showModal = useCommandComponent(Modal)
// 打开两个弹窗
const modal1 = showModal({ title: '弹窗1' })
const modal2 = showModal({ title: '弹窗2' })
// 在 Modal 组件内部:
setup() {
// 两个弹窗都能获取到父组件的 appConfig
const config = inject('appConfig')
// config = { theme: 'dark', version: '1.0' }
// 弹窗1 provide 数据
provide('modalData', 'data from modal1')
// 这个数据只在 modal1 内部有效
// modal2 无法访问到
}