Appearance
第5.2节:provide 与 inject:依赖注入如何实现跨层级通信?
概述
Vue 3 的 provide/inject 机制提供了一种优雅的跨层级组件通信方案,允许祖先组件向其所有子孙组件注入依赖,而无需通过 props 逐层传递。本节将深入分析其源码实现原理。
1. 核心API实现
1.1 provide 函数实现
typescript
// core/packages/runtime-core/src/apiInject.ts
export function provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T,
): void {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
// TS doesn't allow symbol as index type
resolveProvided(currentInstance)[key as string] = value
}
}
function resolveProvided(instance: ComponentInternalInstance): Data {
const existing = instance.provides
const parentProvides = instance.parent && instance.parent.provides
if (parentProvides === existing) {
return (instance.provides = Object.create(parentProvides))
} else {
return existing
}
}核心机制分析:
- 上下文检查:只能在 setup() 函数中调用
- 原型链继承:通过
Object.create(parentProvides)建立继承关系 - 写时复制:首次写入时创建新对象,避免污染父级 provides
1.2 inject 函数实现
typescript
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue?: T,
treatDefaultAsFactory?: false,
): T | undefined
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T | (() => T),
treatDefaultAsFactory: true,
): T
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false,
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance =
currentInstance || currentRenderingInstance || currentApp?._instance
if (instance || currentApp) {
// #2400
// to support `app.runWithContext()` without affecting normal `inject()` calls
const provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}查找机制分析:
- 多上下文支持:支持组件实例、渲染实例和应用实例
- 原型链查找:沿着 provides 原型链向上查找
- 默认值处理:支持静态默认值和工厂函数
- 应用级注入:支持
app.runWithContext()场景
1.3 hasInjectionContext 函数
typescript
export function hasInjectionContext(): boolean {
return !!(currentInstance || currentRenderingInstance || currentApp)
}用于判断当前是否处于可以安全使用 inject 的上下文中。
2. 组件实例中的 provides 属性
2.1 ComponentInternalInstance 接口定义
typescript
// core/packages/runtime-core/src/component.ts
export interface ComponentInternalInstance {
// ...
provides: Data
// ...
}2.2 组件实例创建时的 provides 初始化
typescript
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null,
): ComponentInternalInstance {
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
// ...
provides: parent ? parent.provides : Object.create(appContext.provides),
// ...
}
return instance
}初始化机制:
- 根组件:继承应用上下文的 provides
- 子组件:直接引用父组件的 provides(写时复制)
- 原型链建立:通过
Object.create()建立继承关系
3. 应用级 provide 实现
3.1 AppContext 接口定义
typescript
// core/packages/runtime-core/src/apiCreateApp.ts
export interface AppContext {
app: App // for devtools
config: AppConfig
mixins: ComponentOptions[]
components: Record<string, Component>
directives: Record<string, Directive>
provides: Record<string | symbol, any>
// ...
}3.2 应用上下文创建
typescript
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {},
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap(),
}
}3.3 app.provide 方法实现
typescript
const app: App = (context.app = {
// ...
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
if (hasOwn(context.provides, key as string | symbol)) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`,
)
} else {
// #13212, context.provides can inherit the provides object from parent on custom elements
warn(
`App already provides property with key "${String(key)}" inherited from its parent element. ` +
`It will be overwritten with the new value.`,
)
}
}
context.provides[key as string | symbol] = value
return app
},
// ...
})4. 注入链机制深度解析
4.1 原型链继承模式
typescript
// 祖先组件 provides 结构示例
const grandParentProvides = {
theme: 'dark',
api: apiInstance
}
// 父组件继承祖先组件的 provides
const parentProvides = Object.create(grandParentProvides)
parentProvides.userInfo = currentUser
// 子组件继承父组件的 provides
const childProvides = Object.create(parentProvides)
childProvides.localConfig = config
// 查找过程:child -> parent -> grandParent -> app4.2 写时复制机制
typescript
function resolveProvided(instance: ComponentInternalInstance): Data {
const existing = instance.provides
const parentProvides = instance.parent && instance.parent.provides
// 如果当前 provides 就是父级的引用,说明还没有写入过
if (parentProvides === existing) {
// 创建新对象,建立原型链继承
return (instance.provides = Object.create(parentProvides))
} else {
// 已经创建过自己的 provides 对象
return existing
}
}优势:
- 内存效率:未写入时共享父级对象
- 隔离性:写入时创建独立对象,不影响父级
- 查找效率:利用 JavaScript 原型链的原生查找机制
5. 响应式注入处理
5.1 响应式数据的 provide
typescript
import { ref, reactive, provide, inject } from 'vue'
// 祖先组件
setup() {
const count = ref(0)
const state = reactive({ name: 'Vue' })
// 直接注入响应式对象
provide('count', count)
provide('state', state)
return { count, state }
}
// 后代组件
setup() {
const count = inject('count')
const state = inject('state')
// count 和 state 保持响应式
return { count, state }
}5.2 响应式原理
Vue 3 的 provide/inject 本身不处理响应式,而是直接传递响应式对象的引用:
- ref 对象:传递的是 ref 包装器,保持响应式
- reactive 对象:传递的是 Proxy 对象,保持响应式
- 普通值:传递的是静态值,不具备响应式
6. 默认值处理机制
6.1 静态默认值
typescript
const theme = inject('theme', 'light') // 默认值为 'light'6.2 工厂函数默认值
typescript
const api = inject('api', () => new ApiClient(), true)
// 第三个参数为 true 表示默认值是工厂函数6.3 默认值处理逻辑
typescript
if (provides && (key as string | symbol) in provides) {
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy)
: defaultValue
}7. 类型安全的注入键
7.1 InjectionKey 接口
typescript
// core/packages/runtime-core/src/apiInject.ts
export interface InjectionKey<T> extends Symbol {}7.2 类型安全的使用方式
typescript
import type { InjectionKey } from 'vue'
import { provide, inject } from 'vue'
// 定义类型安全的注入键
const ThemeKey: InjectionKey<string> = Symbol('theme')
const ApiKey: InjectionKey<ApiClient> = Symbol('api')
// 祖先组件
setup() {
provide(ThemeKey, 'dark')
provide(ApiKey, new ApiClient())
}
// 后代组件
setup() {
const theme = inject(ThemeKey) // 类型为 string | undefined
const api = inject(ApiKey, new ApiClient()) // 类型为 ApiClient
}8. 应用场景与最佳实践
8.1 主题系统实现
typescript
// theme.ts
export const ThemeKey: InjectionKey<Theme> = Symbol('theme')
export interface Theme {
primaryColor: string
backgroundColor: string
textColor: string
}
// App.vue
setup() {
const theme = reactive<Theme>({
primaryColor: '#007bff',
backgroundColor: '#ffffff',
textColor: '#333333'
})
provide(ThemeKey, theme)
const toggleTheme = () => {
theme.primaryColor = theme.primaryColor === '#007bff' ? '#28a745' : '#007bff'
}
return { toggleTheme }
}
// 任意子组件
setup() {
const theme = inject(ThemeKey)
const buttonStyle = computed(() => ({
backgroundColor: theme?.primaryColor,
color: theme?.textColor
}))
return { buttonStyle }
}8.2 全局配置管理
typescript
// config.ts
export const ConfigKey: InjectionKey<AppConfig> = Symbol('config')
export interface AppConfig {
apiBaseUrl: string
enableDebug: boolean
locale: string
}
// main.ts
const app = createApp(App)
app.provide(ConfigKey, {
apiBaseUrl: 'https://api.example.com',
enableDebug: process.env.NODE_ENV === 'development',
locale: 'zh-CN'
})
// 服务组件
setup() {
const config = inject(ConfigKey)
const apiClient = new ApiClient(config?.apiBaseUrl)
return { apiClient }
}8.3 插件系统实现
typescript
// plugin.ts
export const PluginKey: InjectionKey<PluginManager> = Symbol('plugin')
export class PluginManager {
private plugins = new Map<string, Plugin>()
register(name: string, plugin: Plugin) {
this.plugins.set(name, plugin)
}
get(name: string): Plugin | undefined {
return this.plugins.get(name)
}
}
// 插件注册
const pluginManager = new PluginManager()
app.provide(PluginKey, pluginManager)
// 插件使用
setup() {
const pluginManager = inject(PluginKey)
const logger = pluginManager?.get('logger')
return { logger }
}9. 性能优化考虑
9.1 避免过度嵌套
typescript
// ❌ 避免过深的嵌套注入
setup() {
const level1 = inject('level1')
const level2 = inject('level2')
const level3 = inject('level3')
// ...
}
// ✅ 使用组合对象
setup() {
const context = inject('appContext', {
level1: defaultLevel1,
level2: defaultLevel2,
level3: defaultLevel3
})
}9.2 合理使用默认值
typescript
// ✅ 提供合理的默认值,避免运行时错误
const theme = inject('theme', () => createDefaultTheme(), true)
const api = inject('api', () => new ApiClient(), true)9.3 避免频繁的 provide
typescript
// ❌ 避免在响应式更新中频繁 provide
watch(someValue, (newValue) => {
provide('dynamicValue', newValue) // 可能导致性能问题
})
// ✅ 使用响应式对象
setup() {
const state = reactive({ dynamicValue: initialValue })
provide('state', state)
watch(someValue, (newValue) => {
state.dynamicValue = newValue // 直接更新响应式对象
})
}10. 与其他通信方式的对比
10.1 vs Props
| 特性 | provide/inject | Props |
|---|---|---|
| 传递层级 | 跨任意层级 | 逐层传递 |
| 类型安全 | 需要 InjectionKey | 原生支持 |
| 性能 | 原型链查找 | 直接访问 |
| 维护性 | 隐式依赖 | 显式声明 |
10.2 vs Event Bus
| 特性 | provide/inject | Event Bus |
|---|---|---|
| 数据流向 | 单向下传 | 双向通信 |
| 生命周期 | 组件绑定 | 全局存在 |
| 内存管理 | 自动清理 | 需手动清理 |
| 调试难度 | 中等 | 较高 |
10.3 vs Vuex/Pinia
| 特性 | provide/inject | 状态管理 |
|---|---|---|
| 适用场景 | 依赖注入 | 全局状态 |
| 学习成本 | 低 | 中等 |
| 功能丰富度 | 基础 | 丰富 |
| 开发工具 | 基础 | 完善 |
总结
Vue 3 的 provide/inject 机制通过原型链继承和写时复制策略,实现了高效的跨层级依赖注入。其核心优势包括:
- 简洁的API:provide/inject 函数简单易用
- 高效的查找:利用 JavaScript 原型链机制
- 内存优化:写时复制避免不必要的对象创建
- 类型安全:通过 InjectionKey 提供类型支持
- 响应式兼容:天然支持响应式数据传递
在实际开发中,provide/inject 适合用于主题系统、全局配置、插件系统等场景,是 Vue 3 组件通信的重要补充。
