Appearance
provide 与 inject:依赖注入如何实现跨层级通信?
概述
props 是组件间通信的基础,但它要求数据必须“逐层传递”。在深层嵌套的组件中,这种方式会变得非常繁琐。
provide 和 inject 提供了一种跨层级组件通信的方案:祖先组件可以“提供”(provide)一个值,其所有子孙组件(无论多深)都可以“注入”(inject)并使用这个值。
这个机制的实现,是巧妙地利用了 JavaScript 原型链的特性。
核心机制:provides 原型链的构建
要理解 provide/inject,首先要看组件实例是如何创建的。当一个子组件被创建时,它的“依赖注入”链就已经被建立好了。
createComponentInstance:建立原型链
在 createComponentInstance 函数中,provides 属性被初始化:
typescript
// core/packages/runtime-core/src/component.ts
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
// ...
): ComponentInternalInstance {
// ...
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
// ...
// 【关键】
// 1. 如果是根组件 (parent=null),则继承 app.provides
// 2. 如果是子组件,则“暂时”直接引用父组件的 provides 对象
provides: parent ? parent.provides : Object.create(appContext.provides),
// ...
}
return instance
}请注意这一行:provides: parent ? parent.provides : ...
- 根组件:它的
provides是Object.create(appContext.provides)。appContext.provides是app.provide注册的地方,是原型链的根。 - 子组件:它的
provides默认直接指向parent.provides。Child.provides === Parent.provides(在provide被调用前)
这个“默认指向父级”的设计,是后续“写时复制”优化的基础。
provide:基于“写时复制” (Copy-on-Write) 的写入
provide 必须在 setup() 期间调用。当一个组件(例如父组件)调用 provide 时,它需要将值写入自己的 provides 对象,同时不能修改父级的 provides。
provide 函数通过 resolveProvided 来获取“可写的” provides 对象,这个过程即“写时复制” (Copy-on-Write)。
typescript
// core/packages/runtime-core/src/apiInject.ts
export function provide<T>(
key: InjectionKey<T> | string,
value: T
): void {
if (!currentInstance) {
// 只能在 setup() 中使用
} else {
// 1. 获取“可写”的 provides 对象
const provides = resolveProvided(currentInstance)
// 2. 写入值
provides[key as string] = value
}
}
/**
* 确保 instance.provides 继承自 parent.provides
* 这就是“写时复制” (Copy-on-Write) 机制
*/
function resolveProvided(instance: ComponentInternalInstance): Data {
const existing = instance.provides
const parentProvides = instance.parent && instance.parent.provides
// 【核心】
// 检查:当前实例的 provides 是否“仍然”是父实例的 provides?
if (parentProvides === existing) {
// 如果是,说明这是“第一次”调用 provide。
// 我们必须创建一个新对象,
// 将这个新对象的“原型”(__proto__)指向父级的 provides,
// 然后将 instance.provides 指向这个新对象。
return (instance.provides = Object.create(parentProvides))
} else {
// 如果不等,说明之前已经创建过,直接返回即可。
return existing
}
}provide 的工作流:
- 父组件
setup:调用provide('theme', 'dark')。 resolveProvided被调用。- 检测到
Parent.provides === GrandParent.provides。 - 执行“写时复制”:
Parent.provides = Object.create(GrandParent.provides)。 Parent.provides现在是一个新对象,其__proto__指向GrandParent.provides。Parent.provides.theme = 'dark',值被写入这个新对象。
inject:在原型链上“读”
provide 负责“写”,inject 负责“读”。inject 的实现非常简洁,因为它利用了 JavaScript 原型链的原生查找能力。
typescript
// core/packages/runtime-core/src/apiInject.ts
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false,
) {
const instance = currentInstance || currentRenderingInstance
if (instance) {
// 【核心】
// 1. 获取“父级”的 provides 链
// (inject 总是从“父级”开始查找,
// 组件不能 inject 自己 provide 的值)
const provides = instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
// 2. 【关键】使用 'in' 操作符
// JavaScript 的 'in' 操作符会自动遍历原型链!
if (provides && (key as string | symbol) in provides) {
// 属性访问 (provides[key]) 同样会自动遍历原型链
return provides[key as string]
} else if (arguments.length > 1) {
// 3. 处理默认值
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance.proxy)
: defaultValue
}
}
}inject 的工作流:
- 子孙组件
setup:调用inject('theme')。 - 获取
instance.parent.provides(父组件的provides对象)。 - 执行
('theme' in parent.provides)。 - JavaScript 引擎执行查找:
- 在
parent.provides对象上查找theme?找到了。返回dark。 - 如果调用
inject('rootValue'):- 在
parent.provides查找?未找到。 - 查找
parent.provides.__proto__(即grandParent.provides)?未找到。 - 查找
grandParent.provides.__proto__(即app.provides)?找到了。返回rootValue。 - 如果一路找到
Object.prototype仍未找到,则返回undefined。
- 在
- 在
inject返回找到的值,或defaultValue。
app.provide:原型链的“根”
app.provide 用于注入“应用级别”的数据,它位于原型链的顶端。
typescript
// core/packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {
return {
// ...
// Object.create(null) 创建一个没有原型的“纯净”对象,
// 它是原型链的终点。
provides: Object.create(null),
// ...
}
}
// app.provide 就是简单地往这个 provides 对象上写属性
app.provide = (key, value) => {
context.provides[key as string | symbol] = value
return app
}当根组件实例创建时,它的 provides 被初始化为 Object.create(appContext.provides),从而将 app.provide 注入的值挂载到了整个组件树的 provides 原型链上。
响应式与类型安全
响应式 (
Reactivity)provide/inject系统本身不处理响应式。它只负责传递引用。provide('theme', 'dark')->inject得到的是静态字符串。provide('theme', ref('dark'))->inject得到的是ref对象。provide('theme', reactive({ color: 'dark' }))->inject得到的是Proxy对象。
响应性是通过在子组件中操作被注入的
ref或reactive对象的引用来保持的。类型安全 (
InjectionKey) 使用字符串作为key容易产生冲突和类型错误。Vue 提供了InjectionKey来解决这个问题。typescript// core/packages/runtime-core/src/apiInject.ts export interface InjectionKey<T> extends Symbol {} // 使用: import type { InjectionKey } from 'vue' // 1. 创建一个“类型化”的 Symbol const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme') // 2. Provide (类型安全) // provide(ThemeKey, 'dark') // <-- TypeScript 会报错 provide(ThemeKey, ref('dark')) // <-- OK // 3. Inject (类型推断) // `theme` 的类型被自动推断为 `Ref<string> | undefined` const theme = inject(ThemeKey)InjectionKey本质上是一个携带了泛型T的Symbol,它利用 TypeScript 的能力,在provide和inject之间建立了类型关联。
总结
Vue 3 的 provide/inject 是一个利用 JavaScript 原型链实现的依赖注入系统:
- 原型链:每个组件的
provides都通过__proto__链接到其父级,最终链接到 App 级的provides。 - 写时复制 (Copy-on-Write):组件默认共享父级的
provides对象。只有当组件自己调用provide时,它才会创建自己的provides对象(Object.create(parentProvides)),并写入这个新对象,从而避免修改父级。 - 原型查找 (Read):
inject(key)只是一个简单的key in parent.provides属性查找,JavaScript 引擎会自动沿着原型链向上搜索,直到找到值或到达链顶。
