Skip to content

provide 与 inject:依赖注入如何实现跨层级通信?

概述

props 是组件间通信的基础,但它要求数据必须“逐层传递”。在深层嵌套的组件中,这种方式会变得非常繁琐。

provideinject 提供了一种跨层级组件通信的方案:祖先组件可以“提供”(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 : ...

  • 根组件:它的 providesObject.create(appContext.provides)appContext.providesapp.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 的工作流:

  1. 父组件 setup:调用 provide('theme', 'dark')
  2. resolveProvided 被调用。
  3. 检测到 Parent.provides === GrandParent.provides
  4. 执行“写时复制”:Parent.provides = Object.create(GrandParent.provides)
  5. Parent.provides 现在是一个新对象,其 __proto__ 指向 GrandParent.provides
  6. 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 的工作流:

  1. 子孙组件 setup:调用 inject('theme')
  2. 获取 instance.parent.provides(父组件的 provides 对象)。
  3. 执行 ('theme' in parent.provides)
  4. JavaScript 引擎执行查找
    • parent.provides 对象上查找 theme找到了。返回 dark
    • 如果调用 inject('rootValue')
      • parent.provides 查找?未找到。
      • 查找 parent.provides.__proto__ (即 grandParent.provides)?未找到。
      • 查找 grandParent.provides.__proto__ (即 app.provides)?找到了。返回 rootValue
      • 如果一路找到 Object.prototype 仍未找到,则返回 undefined
  5. 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 原型链上。

响应式与类型安全

  1. 响应式 (Reactivity)provide/inject 系统本身不处理响应式。它只负责传递引用

    • provide('theme', 'dark') -> inject 得到的是静态字符串。
    • provide('theme', ref('dark')) -> inject 得到的是 ref 对象
    • provide('theme', reactive({ color: 'dark' })) -> inject 得到的是 Proxy 对象

    响应性是通过在子组件中操作被注入的 refreactive 对象的引用来保持的。

  2. 类型安全 (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 本质上是一个携带了泛型 TSymbol,它利用 TypeScript 的能力,在 provideinject 之间建立了类型关联。

总结

Vue 3 的 provide/inject 是一个利用 JavaScript 原型链实现的依赖注入系统:

  1. 原型链:每个组件的 provides 都通过 __proto__ 链接到其父级,最终链接到 App 级的 provides
  2. 写时复制 (Copy-on-Write):组件默认共享父级的 provides 对象。只有当组件自己调用 provide 时,它才会创建自己的 provides 对象Object.create(parentProvides)),并写入这个新对象,从而避免修改父级。
  3. 原型查找 (Read)inject(key) 只是一个简单的 key in parent.provides 属性查找,JavaScript 引擎会自动沿着原型链向上搜索,直到找到值或到达链顶。

微信公众号二维码

Last updated: