主题
实现 provide/inject
本节对标 Vue 3 源码
@vue/runtime-core中的apiInject.ts
provide/inject 是什么?
provide 和 inject 是 Vue 3 中实现跨层级组件通信的 API。祖先组件通过 provide 提供数据,后代组件通过 inject 获取数据,无需逐层传递 props。
GrandParent (provide 'theme')
│
└── Parent(不需要知道 theme 的存在)
│
└── Child (inject 'theme') ← 直接获取这解决了 props drilling 问题 —— 中间层组件不需要为了透传数据而添加不相关的 props。
provide 的实现
对标
packages/runtime-core/src/apiInject.ts——provide
ts
// provide 函数:在当前组件实例上注册一个可供后代组件 inject 的值
export function provide<T>(key: string | symbol, value: T) {
// 获取当前正在执行 setup 的组件实例(只能在 setup 中调用 provide)
const currentInstance = getCurrentInstance()
if (currentInstance) {
let { provides } = currentInstance
// 获取父组件的 provides 用于比较
const parentProvides = currentInstance.parent?.provides
// 关键逻辑:惰性创建 provides 对象
// 初始状态下 instance.provides === parent.provides(指向同一个对象)
// 当组件首次调用 provide 时,才创建一个以父组件 provides 为原型的新对象
// 这样既避免了修改父组件的 provides,又能通过原型链继承所有祖先的 provides
if (provides === parentProvides) {
provides = currentInstance.provides = Object.create(
parentProvides || null,
)
}
// 将 key-value 存储到当前组件自己的 provides 对象上
provides[key] = value
}
}原型链继承机制
这是 provide/inject 最精妙的设计。让我们一步步分析:
初始状态:
在 createComponentInstance 中,组件的 provides 直接指向父组件的 provides:
ts
// 创建组件实例时,provides 直接引用父组件的 provides 对象
// 如果组件不调用 provide,它的 provides 和父组件共享同一个对象(零开销)
function createComponentInstance(vnode, parent) {
const instance = {
// ...
provides: parent ? parent.provides : {}, // 继承父组件的 provides 引用
}
return instance
}首次 provide 时:
ts
if (provides === parentProvides) {
provides = currentInstance.provides = Object.create(parentProvides || null)
}
provides[key] = value使用 Object.create(parentProvides) 创建一个以父组件 provides 为原型的新对象。这样:
- 当前组件的 provide 值存在自身属性上
- 父组件(及更上层)的 provide 值可以通过原型链访问
- 当前组件的 provide 不会影响父组件的 provides 对象
图解原型链
GrandParent.provides = { theme: 'dark', lang: 'zh' }
│
│ Object.create(GrandParent.provides)
▼
Parent.provides = { fontSize: 14 }
│ [[Prototype]] → GrandParent.provides
│
│ Object.create(Parent.provides)
▼
Child.provides = { color: 'red' }
│ [[Prototype]] → Parent.provides
│ │ [[Prototype]] → GrandParent.provides在 Child 中:
Child.provides.color→'red'(自身属性)Child.provides.fontSize→14(原型链 → Parent)Child.provides.theme→'dark'(原型链 → GrandParent)
为什么不直接复制?
ts
// ❌ 错误方式:浅拷贝
provides = currentInstance.provides = { ...parentProvides }
provides[key] = value浅拷贝有两个问题:
- 性能差 — 每个组件都要复制整个 provides 对象
- 无法感知上层变化 — 如果祖先组件后续又 provide 了新值,拷贝的对象不会更新
用原型链:
- 零拷贝 — 只在有 provide 调用时才创建新对象
- 自动继承 — 通过原型链自然获取所有祖先的 provides
没有 provide 的组件
如果一个组件没有调用 provide,它的 provides 就是父组件的 provides(初始化时赋值的引用)。因此不会创建多余的对象:
ts
// createComponentInstance 中
provides: parent ? parent.provides : {}
// 没有调用 provide → provides 始终是 parent.provides 的引用
// 不会触发 Object.createinject 的实现
对标
packages/runtime-core/src/apiInject.ts——inject
ts
// inject 函数:从祖先组件的 provides 中获取指定 key 的值
export function inject<T>(
key: string | symbol, // 要注入的 key,需与 provide 时使用的 key 一致
defaultValue?: T | (() => T), // 可选的默认值,找不到时使用
treatDefaultAsFactory = false, // 是否将函数类型的默认值当作工厂函数执行
): T | undefined {
// 获取当前正在执行 setup 的组件实例
const currentInstance = getCurrentInstance()
if (currentInstance) {
// 从父组件的 provides 中查找(不查找自身的 provides)
// 因为 provide/inject 的语义是"祖先提供,后代消费"
const provides = currentInstance.parent?.provides
// 使用 in 操作符检查 key 是否存在(会沿原型链向上查找)
if (provides && (key as string | symbol) in provides) {
return provides[key as string] // 找到则返回对应的值
} else if (arguments.length > 1) {
// 未找到但提供了默认值
// 如果 treatDefaultAsFactory 为 true 且默认值是函数,则调用工厂函数获取默认值
// 否则直接返回默认值
return treatDefaultAsFactory && typeof defaultValue === 'function'
? (defaultValue as Function)()
: (defaultValue as T)
} else {
// 既未找到值也没有默认值,发出警告
console.warn(`injection "${String(key)}" not found.`)
}
}
}inject 查找的是 parent.provides
注意 inject 查找的是 currentInstance.parent.provides,而非 currentInstance.provides:
ts
const provides = currentInstance.parent?.provides这是因为组件不应该 inject 自己 provide 的值。provide/inject 的语义是"祖先提供,后代消费"。
原型链查找过程
ts
// Child inject('theme')
const provides = Child.parent.provides // → Parent.provides
// Parent.provides 本身没有 theme
// 沿原型链查找:Parent.provides.[[Prototype]] = GrandParent.provides
// GrandParent.provides.theme = 'dark'
// 找到!返回 'dark'JavaScript 的原型链机制天然实现了"沿祖先链向上查找"的逻辑,无需手动遍历组件树。
默认值支持
inject 支持三种默认值形式:
ts
// 1. 无默认值 —— 找不到时警告
const theme = inject('theme')
// 2. 静态默认值
const theme = inject('theme', 'light')
// 3. 工厂函数默认值(避免不必要的对象创建)
const config = inject('config', () => ({ debug: false }), true)工厂函数默认值
第三个参数 treatDefaultAsFactory 控制是否将函数类型的默认值当作工厂函数执行:
ts
// 不作为工厂函数 → 直接返回函数本身
const handler = inject('handler', () => console.log('default'))
// handler 是 () => console.log('default')
// 作为工厂函数 → 执行并返回结果
const config = inject('config', () => ({ debug: false }), true)
// config 是 { debug: false }跨层级通信示例
主题系统
ts
// 根组件:通过 provide 提供主题数据和切换方法
const App = {
setup() {
const theme = ref('dark') // 响应式的主题状态
// 提供主题值,后代组件通过 inject('theme') 获取
provide('theme', theme)
// 提供切换主题的方法,后代组件可以调用来修改主题
provide('toggleTheme', () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
return () => h(Page)
},
}
// 中间组件:完全不需要知道 theme 的存在,避免了 props drilling
const Page = {
setup() {
return () => h('div', {}, [h(Header), h(Content)])
},
}
// 深层子组件:直接通过 inject 获取祖先组件提供的主题数据
const Header = {
setup() {
const theme = inject('theme') // 注入响应式的 theme ref
const toggleTheme = inject('toggleTheme') // 注入切换方法
return () =>
h('header', { class: theme.value }, [
// 点击按钮调用 toggleTheme 切换主题,theme.value 变化会自动触发重渲染
h('button', { onClick: toggleTheme }, 'Toggle Theme'),
])
},
}
// 另一个深层子组件:同样通过 inject 获取主题数据
const Content = {
setup() {
const theme = inject('theme') // 注入同一个 theme ref
return () =>
// theme.value 变化时,这个组件也会自动更新
h('main', { class: `content-${theme.value}` }, 'Content Area')
},
}全局状态管理
provide/inject 可以构建简单的状态管理方案:
ts
// 利用 provide/inject + reactive 构建简单的状态管理方案
function createStore() {
// 使用 reactive 创建响应式状态对象
const state = reactive({
count: 0,
user: null,
})
// 定义操作状态的方法(类似 Vuex 的 mutations/actions)
const actions = {
increment: () => state.count++,
setUser: (user: any) => (state.user = user),
}
return { state, actions }
}
// 根组件:创建 store 并通过 provide 注入
const App = {
setup() {
const store = createStore()
provide('store', store) // 整棵组件树的后代都可以访问 store
return () => h(Child)
},
}
// 任意后代组件:通过 inject 获取 store,使用响应式状态和操作方法
const Child = {
setup() {
// 注入 store,获取 state(reactive 对象)和 actions
const { state, actions } = inject('store')!
return () =>
h('div', {}, [
h('span', {}, String(state.count)), // state.count 变化时自动更新
h('button', { onClick: actions.increment }, '+1'), // 点击按钮调用 increment
])
},
}使用 Symbol 作为 key
推荐使用 Symbol 作为 inject key,避免命名冲突:
ts
// keys.ts
export const ThemeKey = Symbol('theme')
export const I18nKey = Symbol('i18n')
// 提供
provide(ThemeKey, themeRef)
// 注入
const theme = inject(ThemeKey)provide/inject 与响应式
provide 的值可以是响应式的(ref / reactive),inject 获取到的也是同一个响应式引用:
ts
// 祖先组件
const count = ref(0)
provide('count', count)
// 后代组件
const count = inject('count') // 是同一个 ref
// count.value 变化时,后代组件自动更新如果需要防止后代组件修改 provided 的值,可以用 readonly 包装:
ts
provide('count', readonly(count))对比 React useContext
| 维度 | Vue 3 provide/inject | React useContext |
|---|---|---|
| 提供数据 | provide(key, value) | <Context.Provider value={...}> |
| 消费数据 | inject(key, defaultValue) | useContext(Context) |
| 创建方式 | 无需预先创建 | 需要 createContext() |
| key 类型 | string / Symbol | Context 对象 |
| 响应式 | 天然支持(provide ref/reactive) | 需要 state + setState |
| 性能 | 只有消费了值的组件才更新 | Provider value 变化时所有 Consumer 更新 |
| 默认值 | inject 第二个参数 | createContext 参数 |
| 查找机制 | 原型链(JS 原生) | React 内部向上遍历 Fiber 树 |
React Context 的重渲染问题
React 的 Context 有一个知名的性能问题:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,即使它只使用了 value 中的一部分:
tsx
// React —— value 变化会导致所有 consumer 重渲染
const ThemeContext = createContext({ theme: 'dark', lang: 'en' })
function Child() {
const { theme } = useContext(ThemeContext)
// 即使只用了 theme,lang 变化时 Child 也会重渲染
return <div className={theme}>...</div>
}Vue 的 provide/inject 不存在这个问题,因为响应式系统会精确追踪依赖:
ts
// Vue —— 只有 theme.value 变化才会触发使用了 theme 的组件更新
const theme = inject('theme') // ref
// 只有访问 theme.value 的 render effect 才会收集依赖测试用例
ts
describe('provide/inject', () => {
// 测试基本的 provide/inject:父组件 provide,子组件 inject
it('should provide and inject basic value', () => {
let injectedValue: any
const Parent = {
setup() {
provide('msg', 'hello') // 父组件提供 'msg'
return () => h(Child)
},
}
const Child = {
setup() {
injectedValue = inject('msg') // 子组件注入 'msg'
return () => h('div', {}, injectedValue)
},
}
render(h(Parent), document.createElement('div'))
// 断言子组件成功获取到父组件 provide 的值
expect(injectedValue).toBe('hello')
})
// 测试跨层级注入:孙组件可以 inject 到祖父组件 provide 的值
it('should inject from grandparent', () => {
let injectedValue: any
const GrandChild = {
setup() {
injectedValue = inject('msg') // 从祖父组件注入
return () => h('div')
},
}
// 中间层组件不参与 provide/inject
const Child = {
setup() {
return () => h(GrandChild)
},
}
const GrandParent = {
setup() {
provide('msg', 'from grandparent') // 祖父组件提供数据
return () => h(Child)
},
}
render(h(GrandParent), document.createElement('div'))
// 断言通过原型链跨层级成功注入
expect(injectedValue).toBe('from grandparent')
})
// 测试默认值:当 provide 未提供指定 key 时使用默认值
it('should use default value when not provided', () => {
let injectedValue: any
const Child = {
setup() {
// 'missing' key 不存在,使用默认值 'default'
injectedValue = inject('missing', 'default')
return () => h('div')
},
}
const Parent = {
setup() {
return () => h(Child) // 父组件没有 provide 'missing'
},
}
render(h(Parent), document.createElement('div'))
// 断言使用了静态默认值
expect(injectedValue).toBe('default')
})
// 测试工厂函数默认值:第三个参数为 true 时将函数作为工厂函数执行
it('should support factory function as default value', () => {
let injectedValue: any
const Child = {
setup() {
injectedValue = inject(
'config',
() => ({ debug: false }), // 工厂函数
true, // 标记为工厂函数模式
)
return () => h('div')
},
}
const Parent = {
setup() {
return () => h(Child) // 没有 provide 'config'
},
}
render(h(Parent), document.createElement('div'))
// 断言工厂函数被执行,返回了 { debug: false } 对象
expect(injectedValue).toEqual({ debug: false })
})
// 测试就近覆盖:子组件 provide 的同名 key 会覆盖父组件的(对更深层组件而言)
it('should override parent provide with child provide', () => {
let injectedInChild: any
let injectedInGrandChild: any
const GrandChild = {
setup() {
injectedInGrandChild = inject('msg') // 从最近的祖先查找
return () => h('div')
},
}
const Child = {
setup() {
injectedInChild = inject('msg') // 这时查找的是 Parent 的 provides
provide('msg', 'from child') // 然后 Child 自己又 provide 了同名 key
return () => h(GrandChild)
},
}
const Parent = {
setup() {
provide('msg', 'from parent') // 最外层 provide
return () => h(Child)
},
}
render(h(Parent), document.createElement('div'))
// Child inject 到的是 Parent 提供的值
expect(injectedInChild).toBe('from parent')
// GrandChild inject 到的是 Child 提供的值(就近原则)
expect(injectedInGrandChild).toBe('from child')
})
// 测试 provide 不影响父组件:子组件的 provide 不会污染父组件的 provides 对象
it('should not affect parent provides when child provides', () => {
let parentProvides: any
let childProvides: any
const GrandChild = {
setup() {
return () => h('div')
},
}
const Child = {
setup() {
provide('childKey', 'childValue') // 子组件提供新的 key
childProvides = getCurrentInstance()!.provides // 捕获子组件的 provides
return () => h(GrandChild)
},
}
const Parent = {
setup() {
provide('parentKey', 'parentValue') // 父组件提供的 key
parentProvides = getCurrentInstance()!.provides // 捕获父组件的 provides
return () => h(Child)
},
}
render(h(Parent), document.createElement('div'))
// 父组件的 provides 中不应该包含子组件的 key(Object.create 隔离了修改)
expect('childKey' in parentProvides).toBe(false)
// 子组件的 provides 通过原型链能访问到父组件的值
expect(childProvides.parentKey).toBe('parentValue')
// 子组件的 provides 包含自己提供的值
expect(childProvides.childKey).toBe('childValue')
})
// 测试响应式 provide:provide 的 ref 值变化时,inject 的组件自动更新
it('should work with reactive value', async () => {
const count = ref(0) // 响应式值
const Child = {
setup() {
const c = inject<any>('count') // 注入的是同一个 ref 对象
return () => h('span', {}, String(c.value)) // 渲染 ref 的 value
},
}
const Parent = {
setup() {
provide('count', count) // provide 一个 ref
return () => h(Child)
},
}
const root = document.createElement('div')
render(h(Parent), root)
expect(root.innerHTML).toBe('<span>0</span>') // 初始值
count.value = 42 // 修改 ref 值
await nextTick()
// 断言子组件因 ref 值变化而自动重新渲染
expect(root.innerHTML).toBe('<span>42</span>')
})
// 测试 Symbol 类型的 key:推荐方式,避免字符串 key 的命名冲突
it('should support Symbol as injection key', () => {
const key = Symbol('theme') // 创建 Symbol 类型的 key
let injectedValue: any
const Child = {
setup() {
injectedValue = inject(key) // 使用 Symbol key 注入
return () => h('div')
},
}
const Parent = {
setup() {
provide(key, 'dark') // 使用同一个 Symbol key 提供
return () => h(Child)
},
}
render(h(Parent), document.createElement('div'))
// 断言通过 Symbol key 成功完成 provide/inject
expect(injectedValue).toBe('dark')
})
})本节小结
- provide — 将数据存储到
instance.provides,首次调用时通过Object.create创建原型链继承 - inject — 从
parent.provides中查找值,沿原型链自动向上搜索 - 原型链机制 — 利用 JavaScript 原型链实现零成本的跨层级继承,无需手动遍历组件树
- 默认值 — 支持静态默认值和工厂函数默认值
- 响应式 — provide ref/reactive 值,后代组件自动追踪依赖、自动更新
- 跨层级通信 — 解决 props drilling 问题,适用于主题、国际化、全局状态等场景
- 对比 React — Vue 的 provide/inject 依赖精确追踪,不存在 React Context 的全量重渲染问题