Skip to content

第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
  }
}

核心机制分析:

  1. 上下文检查:只能在 setup() 函数中调用
  2. 原型链继承:通过 Object.create(parentProvides) 建立继承关系
  3. 写时复制:首次写入时创建新对象,避免污染父级 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.`)
  }
}

查找机制分析:

  1. 多上下文支持:支持组件实例、渲染实例和应用实例
  2. 原型链查找:沿着 provides 原型链向上查找
  3. 默认值处理:支持静态默认值和工厂函数
  4. 应用级注入:支持 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
}

初始化机制:

  1. 根组件:继承应用上下文的 provides
  2. 子组件:直接引用父组件的 provides(写时复制)
  3. 原型链建立:通过 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 -> app

4.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
  }
}

优势:

  1. 内存效率:未写入时共享父级对象
  2. 隔离性:写入时创建独立对象,不影响父级
  3. 查找效率:利用 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 本身不处理响应式,而是直接传递响应式对象的引用:

  1. ref 对象:传递的是 ref 包装器,保持响应式
  2. reactive 对象:传递的是 Proxy 对象,保持响应式
  3. 普通值:传递的是静态值,不具备响应式

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/injectProps
传递层级跨任意层级逐层传递
类型安全需要 InjectionKey原生支持
性能原型链查找直接访问
维护性隐式依赖显式声明

10.2 vs Event Bus

特性provide/injectEvent Bus
数据流向单向下传双向通信
生命周期组件绑定全局存在
内存管理自动清理需手动清理
调试难度中等较高

10.3 vs Vuex/Pinia

特性provide/inject状态管理
适用场景依赖注入全局状态
学习成本中等
功能丰富度基础丰富
开发工具基础完善

总结

Vue 3 的 provide/inject 机制通过原型链继承和写时复制策略,实现了高效的跨层级依赖注入。其核心优势包括:

  1. 简洁的API:provide/inject 函数简单易用
  2. 高效的查找:利用 JavaScript 原型链机制
  3. 内存优化:写时复制避免不必要的对象创建
  4. 类型安全:通过 InjectionKey 提供类型支持
  5. 响应式兼容:天然支持响应式数据传递

在实际开发中,provide/inject 适合用于主题系统、全局配置、插件系统等场景,是 Vue 3 组件通信的重要补充。


微信公众号二维码