Skip to content

第 14 章 依赖注入与插件系统

本章要点

  • provide/inject 的本质:基于原型链的依赖传递机制
  • 依赖注入的解析过程:从当前组件沿 parent 链向上查找
  • InjectionKey 的类型安全设计:Symbol + 泛型的巧妙结合
  • app.provide 的全局注入:应用级别的依赖如何注入到每一个组件
  • 插件系统的设计哲学:app.use() 如何组织第三方扩展
  • 插件的安装机制:install 函数与重复安装检测
  • 依赖注入在大型应用中的架构价值:替代 props drilling 的优雅方案

依赖注入是 Vue 中最容易被低估的特性之一。很多开发者只在"跨层级传递数据"时才想到 provide/inject,却忽略了它在架构层面的深远意义——它是 Vue 插件系统的基石,是组合式函数共享状态的关键通道,也是 Pinia、Vue Router 等官方库与组件树交互的核心机制。

在前面的章节中,我们深入了解了组件系统的实例化过程和生命周期。本章将揭开组件间数据流动的另一条路径——不是自上而下的 props,也不是自下而上的 emit,而是"穿越"组件层级的依赖注入。

14.1 provide/inject 的基本模型

从问题出发

考虑一个典型的场景:一个主题系统需要将主题配置从根组件传递到任意深度的子组件。

typescript
// 使用 props drilling —— 痛苦的方式
// App → Layout → Sidebar → Menu → MenuItem → Icon
// 每一层都要声明和传递 theme prop

provide/inject 提供了更优雅的方案:

typescript
// 祖先组件
const app = {
  setup() {
    const theme = reactive({ mode: 'dark', primary: '#42b883' })
    provide('theme', theme)
  }
}

// 任意深度的后代组件
const DeepChild = {
  setup() {
    const theme = inject('theme')
    // 直接使用,无需中间组件传递
  }
}

看起来像是"魔法"——数据怎么就"穿越"了中间的组件层级?答案藏在 JavaScript 最基础的机制中。

原型链:依赖注入的底层引擎

Vue 的 provide/inject 底层使用了原型链继承。每个组件实例都有一个 provides 对象,子组件的 provides 的原型指向父组件的 provides

MenuItem 调用 inject('theme') 时,JavaScript 引擎沿原型链向上查找,天然地实现了"跨层级查找"。这个设计的精妙之处在于:

  1. 查找是 O(1) 到 O(n) 的:n 是组件嵌套深度,但实际上 JavaScript 引擎对原型链查找做了高度优化
  2. 中间组件零开销:不需要声明 props,不需要转发数据
  3. 同名覆盖:中间组件可以 provide 同名 key,自然地"遮蔽"祖先的值——就像作用域链中的变量遮蔽

14.2 provide 的实现

源码解析

typescript
// 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 {
    let provides = currentInstance.provides

    // 关键:检查当前组件是否已经"分叉"了 provides 对象
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides

    if (parentProvides === provides) {
      // 第一次在当前组件调用 provide 时
      // 创建一个以父组件 provides 为原型的新对象
      provides = currentInstance.provides = Object.create(parentProvides)
    }

    provides[key as string] = value
  }
}

这段代码的核心逻辑只有几行,但设计极为精巧:

延迟分叉(Lazy Fork)策略:组件实例初始化时,provides 直接指向父组件的 provides(共享引用)。只有当组件第一次调用 provide 时,才通过 Object.create() 创建新对象。这意味着:

  • 不调用 provide 的组件:零内存开销(共享父对象引用)
  • 调用 provide 的组件:只创建一个浅层对象,自身提供的值存在自身对象上,祖先的值通过原型链访问

类型安全的 InjectionKey

Vue 3 提供了 InjectionKey 类型,让 provide/inject 在 TypeScript 中获得完整的类型推导:

typescript
// 定义类型安全的 key
import { InjectionKey } from 'vue'

interface UserService {
  currentUser: Ref<User | null>
  login(credentials: Credentials): Promise<void>
  logout(): void
}

// Symbol 保证全局唯一,泛型参数携带类型信息
export const UserServiceKey: InjectionKey<UserService> = Symbol('UserService')

// provide 时,值必须匹配类型
provide(UserServiceKey, {
  currentUser: ref(null),
  login: async (cred) => { /* ... */ },
  logout: () => { /* ... */ }
})

// inject 时,自动推导类型为 UserService | undefined
const userService = inject(UserServiceKey)
// userService?.currentUser.value  ✓ 类型安全

InjectionKey 的定义极其简洁:

typescript
export interface InjectionKey<T> extends Symbol {}

它本质上就是一个带泛型标记的 Symbol。类型信息只存在于编译时,运行时没有任何开销。这是 TypeScript "零成本抽象"的典型应用。

14.3 inject 的实现

源码解析

typescript
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
export function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: T | (() => T),
  treatDefaultAsFactory?: boolean
): T

export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // 支持在 setup() 和函数式组件中使用
  const instance = currentInstance || currentRenderingInstance

  if (instance || currentApp) {
    // 确定从哪里开始查找
    const provides = currentApp
      ? currentApp._context.provides
      : instance!.parent == null
        ? instance!.vnode.appContext && instance!.vnode.appContext.provides
        : instance!.parent.provides

    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
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  }
}

基于 VitePress 构建