前置知识: JavaScript、HTML5、CSS

Provide 与 Inject

30 min中级

依赖注入与跨层级通信

Provide 与 Inject | Dependency Injection in Vue 3

前置知识

学习目标

  • 掌握「1. 历史动机与发展脉络 | Historical Motivation and Evolution」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义 | Formal Definitions」的核心机制、典型用法与常见陷阱
  • 掌握「3. 理论推导与原理解析 | Theoretical Derivation」的核心机制、典型用法与常见陷阱
  • 掌握「4. 代码示例 | Code Examples」的核心机制、典型用法与常见陷阱
  • 掌握「5. 对比分析 | Comparative Analysis」的核心机制、典型用法与常见陷阱

本文档对标 MIT 6.170、Stanford CS142、CMU 17-437 软件工程课程水准,系统化阐述 Vue 3 中 provide/inject 依赖注入机制的原理、形式化定义、企业级实践与对比分析。涵盖响应式注入、InjectionKey 类型系统、跨层级通信、SSR 单例污染防护、插件架构设计等主题,并辅以数学建模、案例研究与习题。


1. 历史动机与发展脉络 | Historical Motivation and Evolution

1.1 依赖注入模式的起源

依赖注入(Dependency Injection, DI)是控制反转(Inversion of Control, IoC)的一种实现形式,最早由 Martin Fowler 在 2004 年的论文《Inversion of Control Containers and the Dependency Injection pattern》中系统化命名。其核心思想是:对象的依赖由外部容器提供,而非对象自身创建。

DI 模式在企业级 Java(Spring Framework)、.NET(Unity、NInject)、Angular 等框架中广泛应用。Angular 1.x 在前端领域首次将 DI 作为核心架构,2016 年 Angular 2+ 进一步强化了 DI 容器设计。

1.2 Vue 2 时代(2016-2020):初步支持

Vue 2.2 引入 provide/inject API,主要服务于高级组件库开发者。其设计动机:

  1. 跨层级通信需求:组件库中 Form → FormItem → Input 的多层嵌套,使用 props 传递需要逐层声明,造成”prop drilling”问题。
  2. 避免 EventBus 滥用:EventBus 全局事件总线难以追踪数据流,且不保证响应式。
  3. 服务注入:插件需要向应用注入全局服务,但 Vue 2 的 Vue.prototype.$http 方式污染全局原型。

Vue 2 的 provide/inject 限制:

  • provide 是组件选项,必须在 data 之外声明,且非响应式。
  • 若需响应式,必须返回一个引用了 data 的函数,且子组件 inject 后访问的也是同一引用。
// Vue 2 风格(非响应式 provide)
export default {
  provide: {
    theme: 'dark', // 静态值,不会响应
  },
};

// Vue 2 风格(响应式 provide)
export default {
  data() {
    return { theme: 'dark' };
  },
  provide() {
    return {
      theme: this.$data, // 传递整个 data 引用
    };
  },
};

1.3 Vue 3 时代(2020-至今):全面重构

Vue 3 对 provide/inject 进行了根本性重构:

1.3.1 Composition API 整合(Vue 3.0)

provide/inject 成为 Composition API 的一部分,可在 setup() 中以函数形式调用:

import { provide, ref } from 'vue';

export default {
  setup() {
    const theme = ref('dark');
    provide('theme', theme); // 直接传递 ref,自动响应式
    return { theme };
  },
};

1.3.2 InjectionKey 类型系统(Vue 3.0)

引入 InjectionKey<T> 类型,使用 Symbol 作为运行时键,泛型 T 作为编译时类型:

import type { InjectionKey, Ref } from 'vue';

const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme');

provide(ThemeKey, ref('dark'));
const theme = inject(ThemeKey); // 自动推断为 Ref<string> | undefined

1.3.3 默认值与工厂函数(Vue 3.0)

inject 支持第二个参数作为默认值,第三个参数 treatDefaultAsFactory 指示是否将默认值视为工厂函数:

// 静态默认值
const theme = inject('theme', 'light');

// 工厂函数默认值
const config = inject('config', () => createDefaultConfig(), true);

1.3.4 应用级 provide(Vue 3.0)

app.provide() 在应用级别注入,所有组件均可访问:

const app = createApp(App);
app.provide('httpClient', axios);
app.provide('config', { apiBase: '/api' });

1.3.5 SSR 友好(Vue 3.2+)

Vue 3.2+ 优化了 SSR 场景下的 provide/inject,通过 app.provide() 与请求级应用实例隔离,避免单例污染。

1.4 Evan You 的设计哲学

Evan You 对 provide/inject 的定位:

  1. 高级 API,非通用通信方案:provide/inject 主要服务于组件库作者,业务代码应优先使用 props/emits 或 Pinia。
  2. 显式优于隐式:provide/inject 的链式查找是显式的,组件树关系清晰;EventBus 是隐式的,事件来源难以追踪。
  3. 类型安全是关键:InjectionKey<T> 的引入使得 TypeScript 用户能够获得完整的类型推断,避免运行时错误。
  4. 响应式是默认行为:Vue 3 中 provide 的值若为 ref/reactive,则自动响应式,无需额外处理。

1.5 与 React Context 的对比

React Context(2018 年稳定)与 Vue provide/inject 解决相似问题,但实现差异显著:

维度Vue 3 provide/injectReact Context
API 形式provide() 函数 + inject() 函数<Context.Provider value={...}> JSX
类型安全InjectionKey<T> + SymbolcreateContext<T>(defaultValue)
响应式自动(基于 ref/reactive)手动(依赖 useState/useReducer)
查找算法原型链 O(n)Provider 树 O(n)
重渲染粒度精细(依赖追踪)粗放(value 变化全部消费者重渲染)
默认值第二参数,支持工厂函数createContext 时声明
SSR应用级单例污染风险应用级单例污染风险

关键差异:Vue 的响应式系统使得 inject 的组件仅在其依赖的 ref.value 变化时重渲染,而 React Context 的所有消费者在 value 引用变化时全部重渲染,需要通过 useMemo 或拆分 Context 优化。

1.6 与 Angular DI 的对比

Angular 的 DI 容器是框架核心,支持构造函数注入、服务单例、多级注入器(root、module、component):

// Angular
@Injectable({ providedIn: 'root' })
class UserService {}

@Component({})
class MyComponent {
  constructor(private userService: UserService) {}
}

相比之下,Vue 的 provide/inject 更轻量:

  • 无独立 DI 容器,依赖组件树。
  • 无服务单例概念(除 app.provide)。
  • 无依赖声明装饰器,使用 InjectionKey 替代。

Vue 的设计权衡是简单优先:对于 90% 的应用,props/emits + Pinia 已足够,provide/inject 仅作为补充。


2. 形式化定义 | Formal Definitions

2.1 组件树的形式化定义

定义 3.1(组件树):Vue 应用是一个有根的有向树 T=⟨V,E⟩\mathcal{T} = \langle V, E \rangle,其中:

  • VV 是组件实例的集合,根组件 r∈Vr \in V。
  • E⊆V×VE \subseteq V \times V 是父子关系,(p,c)∈E(p, c) \in E 表示 cc 是 pp 的子组件。
  • ∀v∈V∖{r},∃!p∈V:(p,v)∈E\forall v \in V \setminus \{r\}, \exists! p \in V: (p, v) \in E(每个非根组件有唯一父组件)。

定义 3.2(祖先链):对于组件 v∈Vv \in V,其祖先链 ancestors(v)\text{ancestors}(v) 定义为:

ancestors(v)={[]if v=r[p]∪ancestors(p)if v≠r,(p,v)∈E\text{ancestors}(v) = \begin{cases} [] & \text{if } v = r \\ [p] \cup \text{ancestors}(p) & \text{if } v \neq r, (p, v) \in E \end{cases}

2.2 provide 的形式化定义

定义 3.3(provide 操作):provide(key, value) 在组件 vv 上记录一个键值对:

provide:(v,k,val)→providesv[k]:=val\text{provide}: (v, k, \text{val}) \to \text{provides}_v[k] := \text{val}

其中 providesv\text{provides}_v 是组件 vv 的注入表,初始为空对象。

定义 3.4(应用级 provide):app.provide(key, value) 在根应用上记录:

appProvides[k]:=val\text{appProvides}[k] := \text{val}

根组件 rr 的 providesr\text{provides}_r 通过原型链继承自 appProvides\text{appProvides}:

\text{provides}_r.\text{__proto__} = \text{appProvides}

2.3 inject 的形式化定义

定义 3.5(inject 查找算法):inject(key, default?) 在组件 vv 上的查找过程:

inject(v,k)={providesu[k]if ∃u∈ancestors(v)∪{v}:k∈providesudefaultotherwise\text{inject}(v, k) = \begin{cases} \text{provides}_u[k] & \text{if } \exists u \in \text{ancestors}(v) \cup \{v\}: k \in \text{provides}_u \\ \text{default} & \text{otherwise} \end{cases}

查找顺序:从当前组件 vv 开始,沿祖先链向上查找,返回第一个包含 kk 的组件的 provides[k]\text{provides}[k]。

定义 3.6(查找复杂度):设组件树深度为 dd,则 inject 的时间复杂度为:

Tinject(d)=O(d)T_{\text{inject}}(d) = O(d)

在最坏情况下(键不存在),需要遍历从 vv 到根 rr 的所有祖先。

2.4 响应式注入的形式化

定义 3.7(响应式注入值):若 provide 的值 val\text{val} 是响应式对象(ref 或 reactive),则 inject 返回的也是同一引用:

inject(v,k)=val  ⟹  reactive(val)=true\text{inject}(v, k) = \text{val} \implies \text{reactive}(\text{val}) = \text{true}

响应式注入满足以下性质:

  1. 引用一致性:所有 inject 该键的组件获得同一响应式对象引用。
  2. 依赖追踪:组件渲染期间访问 val.value\text{val}.\text{value} 或 val.prop\text{val}.\text{prop} 时,自动建立依赖。
  3. 变更通知:当 val\text{val} 变化时,所有依赖该值的组件触发重渲染。

2.5 InjectionKey 的类型系统

定义 3.8(InjectionKey):InjectionKey<T> 是 Symbol 的子类型:

InjectionKey<T>=Symbol×T\text{InjectionKey}<T> = \text{Symbol} \times T

其中 Symbol 作为运行时键,TT 作为编译时类型约束。

类型推断规则:

  • provide(key: InjectionKey<T>, value: T):编译器检查 value 是否符合类型 TT。
  • inject(key: InjectionKey<T>): T | undefined:返回值自动推断为 T∣undefinedT | \text{undefined}。

定义 3.9(类型安全的注入契约):使用 InjectionKey 的注入契约形式化:

∀k:InjectionKey<T>,∀v:provider:provide(v,k,val)  ⟹  val:T\forall k: \text{InjectionKey}<T>, \forall v: \text{provider}: \text{provide}(v, k, \text{val}) \implies \text{val}: T ∀k:InjectionKey<T>,∀c:consumer:inject(c,k):T∣undefined\forall k: \text{InjectionKey}<T>, \forall c: \text{consumer}: \text{inject}(c, k): T | \text{undefined}

2.6 默认值的形式化

定义 3.10(inject 默认值):inject(key, defaultValue?, treatDefaultAsFactory?) 的语义:

inject(v,k,d,f)={providesu[k]if foundd()if not found and f=truedif not found and f=falseundefinedif not found and d undefined\text{inject}(v, k, d, f) = \begin{cases} \text{provides}_u[k] & \text{if found} \\ d() & \text{if not found and } f = \text{true} \\ d & \text{if not found and } f = \text{false} \\ \text{undefined} & \text{if not found and } d \text{ undefined} \end{cases}

工厂函数模式用于避免默认值的副作用在每次调用时重复执行(如创建新对象)。

2.7 readonly 注入的形式化

定义 3.11(只读注入):通过 readonly() 包装响应式对象,禁止子组件修改:

provide(v,k,readonly(val))  ⟹  ∀c:inject(c,k)=readonly(val)\text{provide}(v, k, \text{readonly}(\text{val})) \implies \forall c: \text{inject}(c, k) = \text{readonly}(\text{val})

readonly 返回一个 Proxy,拦截 set 操作并发出警告:

∀p,val:readonly(val)[p]:=val[p] (read)readonly(val)[p]:=val[p] (write, blocked, warn)\forall p, \text{val}: \text{readonly}(\text{val})[p] := \text{val}[p] \text{ (read)} \\ \text{readonly}(\text{val})[p] := \text{val}[p] \text{ (write, blocked, warn)}

2.8 注入作用域的形式化

定义 3.12(注入作用域):provide 的作用域是从提供者组件及其所有后代组件:

scope(v,k)={v}∪descendants(v)\text{scope}(v, k) = \{v\} \cup \text{descendants}(v)

其中 descendants(v)\text{descendants}(v) 是 vv 的所有后代组件。在作用域外 inject 该键返回 undefined 或默认值。


3. 理论推导与原理解析 | Theoretical Derivation

3.1 provides 原型链的实现机制

Vue 3 内部使用原型链实现 provide/inject 的链式查找。每个组件实例有一个 provides 对象,其原型指向父组件的 provides。

实现伪代码:

// Vue 3 内部实现(简化)
function createComponentInstance(parent) {
  const instance = {
    provides: parent
      ? parent.provides // 子组件默认共享父组件的 provides
      : Object.create(appContext.provides), // 根组件基于 app.provide
    parent,
  };
  return instance;
}

function provide(key, value) {
  const currentInstance = getCurrentInstance();
  if (currentInstance) {
    // 若 provides 仍与父组件共享,则创建新的 provides
    if (currentInstance.provides === currentInstance.parent.provides) {
      currentInstance.provides = Object.create(currentInstance.parent.provides);
    }
    currentInstance.provides[key] = value;
  }
}

function inject(key, defaultValue, treatDefaultAsFactory = false) {
  const instance = getCurrentInstance();
  if (instance) {
    const provides = instance.parent
      ? instance.parent.provides
      : instance.vnode.appContext.provides;
    if (key in provides) {
      return provides[key];
    } else if (defaultValue) {
      return treatDefaultAsFactory ? defaultValue() : defaultValue;
    }
  }
}

关键点:

  1. 延迟创建:子组件的 provides 默认指向父组件的 provides(共享引用),仅当子组件调用 provide 时才创建独立的 provides 对象(通过 Object.create)。
  2. 原型链查找:inject 通过原型链自动向上查找,复杂度 O(d)O(d),dd 为组件深度。
  3. 根组件特例:根组件的 provides 通过 Object.create(appContext.provides) 创建,使得 app.provide 注入的值可被全应用访问。

3.2 响应式注入的依赖追踪

当 inject 返回一个 ref 时,Vue 的响应式系统自动追踪依赖。依赖追踪基于 effect 与 track:

响应式注入的执行流程:

  1. 组件渲染:setup() 执行 inject('theme'),返回 Ref<string>。
  2. 模板求值:模板中 {{ theme }} 触发 theme.value 的 get,进入 track。
  3. 依赖收集:track 将当前渲染 effect 加入 theme 的依赖集合 dep。
  4. 变更触发:父组件修改 theme.value,触发 trigger,遍历 dep 调用所有 effect 的调度函数。
  5. 重渲染:调度函数将组件标记为脏,下一次 flush 时重新渲染。

数学表达:

dep(theme)={e1,e2,…,en}\text{dep}(\text{theme}) = \{e_1, e_2, \ldots, e_n\}

其中 eie_i 是依赖 theme 的渲染 effect。当 theme.value 变化时:

∀ei∈dep(theme):schedule(ei)\forall e_i \in \text{dep}(\text{theme}): \text{schedule}(e_i)

3.3 inject 的查找复杂度分析

设组件树深度为 dd,键 kk 在第 ii 层(0≤i≤d0 \leq i \leq d)被 provide(i=0i=0 表示根组件),则 inject 的查找步数为 d−id - i。

平均情况(假设 provide 在各层均匀分布):

E[steps]=1d+1∑i=0d(d−i)=d2E[\text{steps}] = \frac{1}{d+1} \sum_{i=0}^{d} (d - i) = \frac{d}{2}

最坏情况(键不存在或仅在根组件):

Tworst(d)=O(d)T_{\text{worst}}(d) = O(d)

优化建议:

  • 对于频繁 inject 的值,优先 provide 在靠近消费者的组件,减少查找深度。
  • 对于全局不变的值(如配置),使用 app.provide 注入根组件,避免重复 provide。

3.4 与 React Context 的性能对比

React Context 的重渲染机制基于 Context.Provider 的 value 引用比较:

  • value 是新对象 → 所有 useContext 消费者重渲染。
  • value 是同一对象引用 → 消费者不重渲染。

问题:若 value 是 { theme: 'dark', locale: 'zh' },每次 Provider 重渲染时 value 引用变化,所有消费者(即使只用 theme)都重渲染。

Vue 的优势:响应式注入基于属性级依赖追踪:

  • 父组件 provide('config', reactive({ theme: 'dark', locale: 'zh' }))。
  • 子组件 inject('config').theme 仅依赖 theme 属性。
  • 父组件修改 config.locale 时,仅依赖 locale 的子组件重渲染,依赖 theme 的不重渲染。

性能差异量化:

设组件树有 nn 个消费者,每个消费者依赖 value 的 kk 个属性中的 1 个。

  • React Context:每次 value 变化,nn 个消费者全部重渲染。
  • Vue provide/inject:仅依赖变化属性的消费者重渲染,平均 n/kn/k 个。
speedup=nn/k=k\text{speedup} = \frac{n}{n/k} = k

3.5 SSR 单例污染的理论分析

在 SSR 中,app.provide() 注入的值在应用实例上,若同一应用实例服务多个请求,会导致数据污染。

单例污染示例:

// server.js
import { createSSRApp } from 'vue';

const app = createSSRApp(App);
app.provide('user', null); // 全局单例

// 请求 1:登录用户 A
app.provides.user = { name: 'Alice' };

// 请求 2:期望 user 为 null,但实际为 Alice(污染!)

解决方案:

  1. 请求级应用实例:每个请求创建独立的 app 实例:
export function render() {
  const app = createSSRApp(App);
  app.provide('user', getCurrentUser());
  return renderToString(app);
}
  1. 工厂函数注入:使用 inject(key, factory, true) 在消费者侧按需创建:
const user = inject('user', () => createUser(), true);
  1. Symbol 隔离:每个请求生成唯一 Symbol 作为键:
const requestSymbol = Symbol('request');
provide(requestSymbol, requestData);

复杂度分析:

  • 请求级应用实例:每次请求 O(1)O(1) 创建应用,内存占用 O(requests)O(\text{requests})。
  • 工厂函数注入:每次 inject O(1)O(1) 创建值,内存占用 O(consumers)O(\text{consumers})。
  • Symbol 隔离:每个请求 O(1)O(1) 创建 Symbol,但需要传递 Symbol,复杂度高。

Nuxt 3 采用请求级应用实例 + useNuxtApp() composable 封装,是当前 SSR DI 的最佳实践。

3.6 InjectionKey 的类型推断原理

InjectionKey<T> 的类型推断基于 TypeScript 的泛型与 Symbol 唯一性:

// Vue 3 源码
export interface InjectionKey<T> extends Symbol {}

export function provide<T, K = InjectionKey<T> | string | number>(
  key: K,
  value: K extends InjectionKey<infer V> ? V : T
): void;

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

类型推断流程:

  1. 定义 Key:const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme')。
  2. provide 类型检查:provide(ThemeKey, value) 中,编译器从 InjectionKey<Ref<string>> 推断 value: Ref<string>。
  3. inject 类型推断:inject(ThemeKey) 返回 Ref<string> | undefined(因为可能未找到)。

类型安全收益:

  • 编译时捕获类型不匹配:provide(ThemeKey, 123) 报错(number 不兼容 Ref<string>)。
  • 消费者无需手动断言:inject(ThemeKey) 自动推断为 Ref<string> | undefined,无需 as 断言。
  • 跨文件共享 Key 时保持类型一致性:导出 InjectionKey 即导出类型契约。

3.7 readonly 注入的拦截机制

readonly() 返回一个 Proxy,拦截 set、delete 操作:

// Vue 3 内部实现(简化)
function readonly(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key); // 依赖追踪
      return result;
    },
    set(target, key, value, receiver) {
      console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`);
      return true; // 阻止修改
    },
    deleteProperty(target, key) {
      console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`);
      return true;
    },
  });
}

readonly 的语义:

  • 浅层只读:仅顶层属性不可修改,嵌套对象仍可修改。
  • 深层只读:使用 readonly(reactive(obj)) 实现深层只读。

readonly 注入的工程价值:

  1. 封装修改权:父组件提供 readonly 后,子组件只能读取,修改权集中在父组件。
  2. 单向数据流:明确数据流向,避免子组件直接修改导致的状态混乱。
  3. 调试友好:若子组件尝试修改,控制台立即警告,便于定位问题。

4. 代码示例 | Code Examples

4.1 基础用法:主题切换

<!-- App.vue —— Vue 3.4+ -->
<script setup>
import { provide, ref, readonly } from 'vue';
import ThemedComponent from './ThemedComponent.vue';

// 创建响应式主题状态
const theme = ref('dark');

// 切换主题的方法
function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark';
}

// 提供主题状态(只读,避免子组件直接修改)
provide('theme', readonly(theme));
// 提供切换方法(允许子组件调用)
provide('toggleTheme', toggleTheme);
</script>

<template>
  <div :class="['app', `theme-${theme}`]">
    <h1>Theme: {{ theme }}</h1>
    <button @click="toggleTheme">Toggle Theme</button>
    <ThemedComponent />
  </div>
</template>

<style scoped>
.app.theme-dark {
  background: #1a1a1a;
  color: #f0f0f0;
}
.app.theme-light {
  background: #ffffff;
  color: #1a1a1a;
}
</style>
vue
<!-- ThemedComponent.vue —— 子组件(深层嵌套) -->
<script setup>
import { inject } from 'vue';

// 注入主题状态(只读 Ref<string>)
const theme = inject('theme');
// 注入切换方法
const toggleTheme = inject('toggleTheme');
</script>

<template>
  <div class="themed">
    <p>Current theme: {{ theme }}</p>
    <button @click="toggleTheme">Switch Theme</button>
  </div>
</template>

<style scoped>
.themed {
  padding: 16px;
  border: 1px solid currentColor;
  border-radius: 4px;
}
</style>

4.2 类型安全:InjectionKey 与 Symbol

// keys/theme.ts —— 集中管理 InjectionKey
import type { InjectionKey, Ref } from 'vue';

export interface ThemeContext {
  theme: Readonly<Ref<string>>;
  toggleTheme: () => void;
}

export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme');

export const LocaleKey: InjectionKey<{
  locale: Ref<string>;
  t: (key: string) => string;
}> = Symbol('locale');

export const UserKey: InjectionKey<{
  user: Readonly<Ref<User | null>>;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => Promise<void>;
}> = Symbol('user');
typescript
// composables/useTheme.ts —— 封装为 Composable,提供友好 API
import { inject } from 'vue';
import { ThemeKey } from '../keys/theme';

export function useTheme() {
  const context = inject(ThemeKey);
  if (!context) {
    throw new Error('useTheme() must be called within a component that provides ThemeKey');
  }
  return context;
}
vue
<!-- App.vue —— 提供 ThemeContext -->
<script setup lang="ts">
import { provide, ref, readonly } from 'vue';
import { ThemeKey } from './keys/theme';

const theme = ref<'dark' | 'light'>('dark');

function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark';
}

provide(ThemeKey, {
  theme: readonly(theme),
  toggleTheme,
});
</script>

4.3 响应式注入:计数器

<!-- CounterProvider.vue —— Vue 3.4+ -->
<script setup>
import { provide, reactive, readonly, computed } from 'vue';
import CounterDisplay from './CounterDisplay.vue';
import CounterControls from './CounterControls.vue';

// 创建响应式状态
const state = reactive({
  count: 0,
  history: [],
});

// 计算属性
const doubleCount = computed(() => state.count * 2);

// 修改方法
function increment() {
  state.history.push(state.count);
  state.count++;
}

function decrement() {
  if (state.count > 0) {
    state.history.push(state.count);
    state.count--;
  }
}

function reset() {
  state.history.push(state.count);
  state.count = 0;
}

// 提供只读状态与修改方法
provide('counter', {
  state: readonly(state),
  doubleCount,
  increment,
  decrement,
  reset,
});
</script>

<template>
  <div class="counter-provider">
    <h2>Counter Provider</h2>
    <CounterDisplay />
    <CounterControls />
  </div>
</template>
vue
<!-- CounterDisplay.vue —— 仅消费状态 -->
<script setup>
import { inject } from 'vue';

const { state, doubleCount } = inject('counter');
</script>

<template>
  <div class="counter-display">
    <p>Count: {{ state.count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <p>History: {{ state.history.join(', ') || 'empty' }}</p>
  </div>
</template>
vue
<!-- CounterControls.vue —— 仅消费方法 -->
<script setup>
import { inject } from 'vue';

const { increment, decrement, reset } = inject('counter');
</script>

<template>
  <div class="counter-controls">
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

4.4 企业级表单组件库

// form/keys.ts —— Form 组件库的 InjectionKey
import type { InjectionKey, Ref, Reactive } from 'vue';

export interface FormItemContext {
  prop: string;
  label: string;
  required: boolean;
  rules: FormRule[];
  validate: () => Promise<boolean>;
  resetField: () => void;
  clearValidate: () => void;
  error: Ref<string>;
  validating: Ref<boolean>;
}

export interface FormContext {
  model: Reactive<Record<string, any>>;
  rules: Record<string, FormRule[]>;
  labelWidth: string;
  labelPosition: 'left' | 'right' | 'top';
  addField: (field: FormItemContext) => void;
  removeField: (field: FormItemContext) => void;
  validate: (callback?: (valid: boolean) => void) => Promise<boolean>;
  validateField: (prop: string, callback?: (valid: boolean) => void) => Promise<boolean>;
  resetFields: () => void;
  clearValidate: (props?: string | string[]) => void;
}

export const FormKey: InjectionKey<FormContext> = Symbol('form');
export const FormItemKey: InjectionKey<FormItemContext> = Symbol('form-item');
vue
<!-- form/Form.vue —— 表单容器组件 -->
<script setup lang="ts">
import { provide, reactive, ref, onUnmounted } from 'vue';
import { FormKey } from './keys';

const props = withDefaults(defineProps<{
  model: Record<string, any>;
  rules?: Record<string, FormRule[]>;
  labelWidth?: string;
  labelPosition?: 'left' | 'right' | 'top';
}>(), {
  labelWidth: '80px',
  labelPosition: 'right',
});

const fields: FormItemContext[] = [];

function addField(field: FormItemContext) {
  fields.push(field);
}

function removeField(field: FormItemContext) {
  const index = fields.indexOf(field);
  if (index > -1) fields.splice(index, 1);
}

async function validate(callback?: (valid: boolean) => void): Promise<boolean> {
  const results = await Promise.all(
    fields.map((field) => field.validate().catch(() => false))
  );
  const valid = results.every(Boolean);
  callback?.(valid);
  return valid;
}

async function validateField(prop: string, callback?: (valid: boolean) => void): Promise<boolean> {
  const field = fields.find((f) => f.prop === prop);
  if (!field) return true;
  const valid = await field.validate();
  callback?.(valid);
  return valid;
}

function resetFields() {
  fields.forEach((field) => field.resetField());
}

function clearValidate(props?: string | string[]) {
  const targetFields = props
    ? fields.filter((f) => Array.isArray(props) ? props.includes(f.prop) : f.prop === props)
    : fields;
  targetFields.forEach((field) => field.clearValidate());
}

provide(FormKey, {
  model: reactive(props.model),
  rules: props.rules || {},
  labelWidth: props.labelWidth,
  labelPosition: props.labelPosition,
  addField,
  removeField,
  validate,
  validateField,
  resetFields,
  clearValidate,
});
</script>

<template>
  <form class="fandex-form" @submit.prevent="validate">
    <slot />
  </form>
</template>

<style scoped>
.fandex-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
</style>
vue
<!-- form/FormItem.vue —— 表单项组件 -->
<script setup lang="ts">
import { provide, inject, ref, onMounted, onUnmounted, computed } from 'vue';
import { FormKey, FormItemKey, type FormItemContext } from './keys';

const props = defineProps<{
  prop: string;
  label: string;
  required?: boolean;
  rules?: FormRule[];
}>();

const form = inject(FormKey);
if (!form) {
  throw new Error('FormItem must be used within a Form');
}

const error = ref('');
const validating = ref(false);

async function validate(): Promise<boolean> {
  if (!form) return true;
  validating.value = true;
  error.value = '';
  const value = form.model[props.prop];
  const rules = props.rules || form.rules[props.prop] || [];
  for (const rule of rules) {
    if (rule.required && !value) {
      error.value = rule.message || `${props.label} is required`;
      validating.value = false;
      return false;
    }
    if (rule.validator) {
      try {
        await rule.validator(value, form.model);
      } catch (err) {
        error.value = (err as Error).message || rule.message || 'Validation failed';
        validating.value = false;
        return false;
      }
    }
  }
  validating.value = false;
  return true;
}

function resetField() {
  if (form) {
    form.model[props.prop] = undefined;
  }
  error.value = '';
}

function clearValidate() {
  error.value = '';
}

const context: FormItemContext = {
  prop: props.prop,
  label: props.label,
  required: props.required || false,
  rules: props.rules || [],
  validate,
  resetField,
  clearValidate,
  error,
  validating,
};

provide(FormItemKey, context);

onMounted(() => {
  form?.addField(context);
});

onUnmounted(() => {
  form?.removeField(context);
});

const labelStyle = computed(() => ({
  width: form?.labelWidth,
  textAlign: form?.labelPosition,
}));
</script>

<template>
  <div class="fandex-form-item" :class="{ 'has-error': error }">
    <label class="fandex-form-item__label" :style="labelStyle">
      <span v-if="required" class="required-mark">*</span>
      {{ label }}
    </label>
    <div class="fandex-form-item__content">
      <slot />
      <div v-if="error" class="fandex-form-item__error">{{ error }}</div>
    </div>
  </div>
</template>

<style scoped>
.fandex-form-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.fandex-form-item__label {
  font-weight: 500;
  padding-top: 6px;
}
.required-mark {
  color: #f56c6c;
  margin-right: 4px;
}
.fandex-form-item__content {
  flex: 1;
}
.fandex-form-item__error {
  color: #f56c6c;
  font-size: 12px;
  margin-top: 4px;
}
</style>

4.5 国际化(i18n)系统

// i18n/keys.ts
import type { InjectionKey, Ref, ComputedRef } from 'vue';

export interface I18nContext {
  locale: Ref<string>;
  availableLocales: string[];
  t: (key: string, params?: Record<string, any>) => string;
  setLocale: (locale: string) => void;
  fallbackLocale: ComputedRef<string>;
}

export const I18nKey: InjectionKey<I18nContext> = Symbol('i18n');
typescript
// i18n/index.ts —— i18n 插件实现
import { ref, computed, provide, inject } from 'vue';
import { I18nKey, type I18nContext } from './keys';

const messages = {
  'zh-CN': {
    'app.title': 'FANDEX 知识库',
    'app.welcome': '欢迎,{name}!',
    'button.save': '保存',
    'button.cancel': '取消',
    'form.required': '{field}为必填项',
  },
  'en-US': {
    'app.title': 'FANDEX Knowledge Base',
    'app.welcome': 'Welcome, {name}!',
    'button.save': 'Save',
    'button.cancel': 'Cancel',
    'form.required': '{field} is required',
  },
};

export function createI18n(options: { locale: string; fallback?: string }) {
  const locale = ref(options.locale);
  const fallback = computed(() => options.fallback || 'en-US');

  function t(key: string, params?: Record<string, any>): string {
    const dict = messages[locale.value] || messages[fallback.value];
    let message = dict[key] || messages[fallback.value][key] || key;
    if (params) {
      Object.entries(params).forEach(([k, v]) => {
        message = message.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
      });
    }
    return message;
  }

  function setLocale(newLocale: string) {
    locale.value = newLocale;
    document.documentElement.lang = newLocale;
  }

  const context: I18nContext = {
    locale,
    availableLocales: Object.keys(messages),
    t,
    setLocale,
    fallbackLocale: fallback,
  };

  return {
    install(app) {
      app.provide(I18nKey, context);
    },
    context,
  };
}

export function useI18n(): I18nContext {
  const context = inject(I18nKey);
  if (!context) {
    throw new Error('useI18n() must be called within an app with i18n plugin installed');
  }
  return context;
}
typescript
// main.ts —— 应用入口
import { createApp } from 'vue';
import App from './App.vue';
import { createI18n } from './i18n';

const i18n = createI18n({
  locale: navigator.language || 'zh-CN',
  fallback: 'en-US',
});

const app = createApp(App);
app.use(i18n);
app.mount('#app');
vue
<!-- components/LocalizedText.vue —— 使用 i18n -->
<script setup lang="ts">
import { useI18n } from '../i18n';

const { t, locale, setLocale, availableLocales } = useI18n();
</script>

<template>
  <div class="localized">
    <h1>{{ t('app.title') }}</h1>
    <p>{{ t('app.welcome', { name: 'FANDEX' }) }}</p>
    <select v-model="locale" @change="setLocale(locale)">
      <option v-for="loc in availableLocales" :key="loc" :value="loc">
        {{ loc }}
      </option>
    </select>
  </div>
</template>

4.6 用户认证系统

// auth/keys.ts
import type { InjectionKey, Ref, ComputedRef } from 'vue';

export interface User {
  id: string;
  name: string;
  email: string;
  roles: string[];
  permissions: string[];
}

export interface AuthContext {
  user: Readonly<Ref<User | null>>;
  isAuthenticated: ComputedRef<boolean>;
  hasRole: (role: string) => boolean;
  hasPermission: (permission: string) => boolean;
  login: (credentials: { email: string; password: string }) => Promise<void>;
  logout: () => Promise<void>;
  refresh: () => Promise<void>;
}

export const AuthKey: InjectionKey<AuthContext> = Symbol('auth');
typescript
// auth/index.ts
import { ref, computed, provide, inject, readonly } from 'vue';
import { AuthKey, type AuthContext, type User } from './keys';
import { httpClient } from './http';

export function createAuth() {
  const user = ref<User | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);

  const isAuthenticated = computed(() => user.value !== null);

  function hasRole(role: string): boolean {
    return user.value?.roles.includes(role) ?? false;
  }

  function hasPermission(permission: string): boolean {
    return user.value?.permissions.includes(permission) ?? false;
  }

  async function login(credentials: { email: string; password: string }) {
    loading.value = true;
    error.value = null;
    try {
      const response = await httpClient.post('/auth/login', credentials);
      user.value = response.user;
      localStorage.setItem('token', response.token);
    } catch (err) {
      error.value = (err as Error).message;
      throw err;
    } finally {
      loading.value = false;
    }
  }

  async function logout() {
    await httpClient.post('/auth/logout');
    user.value = null;
    localStorage.removeItem('token');
  }

  async function refresh() {
    const token = localStorage.getItem('token');
    if (!token) return;
    try {
      const response = await httpClient.get('/auth/me');
      user.value = response;
    } catch {
      user.value = null;
      localStorage.removeItem('token');
    }
  }

  const context: AuthContext = {
    user: readonly(user),
    isAuthenticated,
    hasRole,
    hasPermission,
    login,
    logout,
    refresh,
  };

  return {
    install(app) {
      app.provide(AuthKey, context);
      // 启动时尝试刷新用户状态
      refresh();
    },
    context,
  };
}

export function useAuth(): AuthContext {
  const context = inject(AuthKey);
  if (!context) {
    throw new Error('useAuth() must be called within an app with auth plugin installed');
  }
  return context;
}

4.7 插件开发:HTTP 客户端注入

// plugins/httpClient.ts
import type { InjectionKey } from 'vue';
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';

export interface HttpClientConfig {
  baseURL: string;
  timeout?: number;
  headers?: Record<string, string>;
}

export const HttpClientKey: InjectionKey<AxiosInstance> = Symbol('http-client');

export function createHttpClient(config: HttpClientConfig) {
  const client = axios.create({
    baseURL: config.baseURL,
    timeout: config.timeout ?? 10000,
    headers: config.headers,
  });

  // 请求拦截器:附加 token
  client.interceptors.request.use((requestConfig) => {
    const token = localStorage.getItem('token');
    if (token) {
      requestConfig.headers.Authorization = `Bearer ${token}`;
    }
    return requestConfig;
  });

  // 响应拦截器:统一错误处理
  client.interceptors.response.use(
    (response) => response.data,
    (error) => {
      if (error.response?.status === 401) {
        localStorage.removeItem('token');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    }
  );

  return {
    install(app) {
      app.provide(HttpClientKey, client);
    },
    client,
  };
}

export function useHttpClient(): AxiosInstance {
  const client = inject(HttpClientKey);
  if (!client) {
    throw new Error('useHttpClient() must be called within an app with httpClient plugin installed');
  }
  return client;
}

4.8 工厂函数默认值

// 当 inject 未找到时,使用工厂函数创建默认值
import { inject, reactive } from 'vue';

// 配置类型
interface AppConfig {
  apiBase: string;
  features: {
    analytics: boolean;
    notifications: boolean;
  };
  theme: {
    primary: string;
    secondary: string;
  };
}

function createDefaultConfig(): AppConfig {
  return reactive({
    apiBase: '/api',
    features: {
      analytics: false,
      notifications: true,
    },
    theme: {
      primary: '#007bff',
      secondary: '#6c757d',
    },
  });
}

// 使用工厂函数作为默认值
const config = inject('config', createDefaultConfig, true);

// 每次 inject 调用都会执行工厂函数,返回独立的默认配置
// 适用于需要隔离状态的场景

4.9 应用级 provide 与插件

// main.ts —— Vue 3.4+ 应用入口
import { createApp } from 'vue';
import App from './App.vue';
import { createI18n } from './i18n';
import { createAuth } from './auth';
import { createHttpClient } from './plugins/httpClient';

const app = createApp(App);

// 注册插件,内部使用 app.provide
app.use(createHttpClient({ baseURL: import.meta.env.VITE_API_BASE }));
app.use(createI18n({ locale: 'zh-CN', fallback: 'en-US' }));
app.use(createAuth());

// 直接 app.provide 全局配置
app.provide('appConfig', {
  version: '1.0.0',
  environment: import.meta.env.MODE,
  features: {
    beta: import.meta.env.VITE_ENABLE_BETA === 'true',
  },
});

app.mount('#app');

4.10 调试:可视化注入树

// composables/useProvideDebug.ts —— 开发模式下记录 provide/inject 调用
import { getCurrentInstance, onMounted } from 'vue';

const DEBUG = import.meta.env.DEV;

export function debugProvide(key: string | symbol, value: unknown) {
  if (!DEBUG) return;
  const instance = getCurrentInstance();
  if (!instance) return;
  
  onMounted(() => {
    // 在 Vue Devtools 中显示
    console.debug(
      `[provide] ${String(key)} in <${instance.type.name || 'Anonymous'}>`,
      value
    );
  });
}

export function debugInject(key: string | symbol) {
  if (!DEBUG) return;
  const instance = getCurrentInstance();
  if (!instance) return;
  
  console.debug(
    `[inject] ${String(key)} in <${instance.type.name || 'Anonymous'}>`
  );
}

5. 对比分析 | Comparative Analysis

5.1 与 Props/Emit 的对比

维度provide/injectprops/emit
通信方向父→子(任意深度)父↔子(仅相邻层)
类型安全InjectionKeydefineProps()
响应式自动(ref/reactive)自动(props 是响应式)
适用场景跨层级共享父子直接通信
可测试性需 mock provide直接传 props
可追溯性链式查找,较难追踪显式传递,易追踪
重渲染粒度精细(属性级)精细(prop 变化)
学习成本中等低

选择建议:

  • 跨 3 层以上的组件通信 → provide/inject。
  • 仅父子通信 → props/emit。
  • 需要全局共享 → Pinia。
  • 组件库内部协作 → provide/inject。

5.2 与 EventBus 的对比

维度provide/injectEventBus
通信模式树状注入发布订阅
作用域组件子树全局
类型安全强(InjectionKey)弱(字符串事件)
响应式是否(需手动)
调试Vue Devtools 可视化事件流难追踪
内存管理自动(组件卸载)手动(需 off)
推荐度高低(已不推荐)

EventBus 的问题:

  • 事件来源不可追溯,难以调试。
  • 全局事件易冲突,难以维护。
  • 无响应式,需手动触发更新。
  • Vue 3 已移除官方 EventBus($on、$off),推荐使用 mitt 等第三方库或迁移到 provide/inject + Pinia。

5.3 与 Pinia/Vuex 的对比

维度provide/injectPinia
作用域组件子树全局(或模块)
持久化否(需手动)支持(pinia-plugin-persistedstate)
Devtools有限支持完整支持(时间旅行)
SSR 友好需注意单例污染自动隔离
类型安全InjectionKey完整 TypeScript 支持
适用场景局部共享、插件全局状态管理
学习成本低中
生态Vue 原生丰富插件

选择建议:

  • 用户登录态、主题、国际化 → provide/inject(与组件树绑定)。
  • 购物车、商品列表、全局计数 → Pinia(需要 Devtools 与持久化)。
  • 表单状态、对话框状态 → 视复杂度,简单用 provide/inject,复杂用 Pinia。

5.4 与 React Context 的对比

维度Vue provide/injectReact Context
API 形式函数式JSX Provider
类型安全InjectionKeycreateContext
默认值inject 第二参数createContext 第一参数
响应式自动手动(useState/useReducer)
重渲染粒度属性级Provider value 引用级
性能优化自动需 useMemo/拆分 Context
作用域组件子树Provider 子树
Hook 支持Composable 包装useContext Hook
SSR单例污染风险单例污染风险

关键差异:

  1. 重渲染性能:Vue 的响应式系统天然属性级追踪,React Context 需手动优化。
  2. API 风格:Vue 是函数式,React 是 JSX,各有优劣。
  3. 默认值:Vue 在 inject 时声明,React 在 createContext 时声明,后者更集中。

5.5 与 Angular DI 的对比

维度Vue provide/injectAngular DI
容器组件树独立 DI 容器
注入方式函数调用构造函数参数
作用域组件子树root/module/component
单例app.provideprovidedIn: ‘root’
多实例组件级 provideproviders: []
类型安全InjectionKeyTypeScript 类型
装饰器无@Injectable, @Inject
学习成本低高

Angular DI 的优势:

  • 完整的 DI 容器,支持服务生命周期管理。
  • 构造函数注入,依赖关系显式。
  • 多级注入器,灵活的作用域控制。

Vue 的设计哲学:

  • 简单优先,避免过度工程化。
  • 组件树作为天然 DI 容器,无需额外抽象。
  • InjectionKey 提供类型安全,不引入装饰器复杂度。

5.6 与 Svelte Context 的对比

维度Vue provide/injectSvelte Context
APIprovide/injectsetContext/getContext
类型安全InjectionKey泛型参数
响应式自动需 store 包装
调用时机setup 同步组件初始化
查找原型链组件树向上

Svelte 的 setContext/getContext 与 Vue 非常相似,但 Svelte 的响应式基于编译时,Vue 基于运行时 Proxy。

5.7 综合选型决策矩阵

场景推荐方案
父子直接通信props/emit
跨 3 层以上共享provide/inject
全局状态(用户、购物车)Pinia
插件配置注入app.provide
组件库内部协作provide/inject + InjectionKey
跨组件事件通知Pinia + watch 或 mitt
表单状态provide/inject(组件库)或 Pinia(业务)
主题/国际化provide/inject
SSR 友好的请求级状态请求级 app.provide 或 Nuxt useNuxtApp

6. 常见陷阱与最佳实践 | Pitfalls and Best Practices

6.1 陷阱:响应式丢失

错误代码:

// 父组件
const theme = ref('dark');
provide('theme', theme.value); // 错误!传递的是值,非 ref

// 子组件
const theme = inject('theme'); // theme 是字符串 'dark',无响应式

正确做法:

// 父组件
const theme = ref('dark');
provide('theme', theme); // 传递 ref 本身

// 子组件
const theme = inject('theme'); // theme 是 Ref<string>,响应式

原理:provide 接收的是值的引用,若传递 theme.value,则传递的是字符串值,失去响应式。

6.2 陷阱:解构失去响应式

错误代码:

const theme = inject('theme'); // Ref<string>
const { value } = theme; // 错误!value 是字符串,失去响应式

正确做法:

const theme = inject('theme'); // Ref<string>
// 直接使用 theme.value,保持响应式
// 或使用 toRefs 解构对象

6.3 陷阱:命名冲突

错误代码:

// 父组件 A
provide('config', { apiBase: '/api' });

// 父组件 B(A 的子组件)
provide('config', { apiBase: '/api/v2' }); // 覆盖!

// 孙组件
const config = inject('config'); // 获取的是 B 的 config

正确做法:

  1. 使用 Symbol:
const ParentAConfigKey: InjectionKey<Config> = Symbol('parent-a-config');
const ParentBConfigKey: InjectionKey<Config> = Symbol('parent-b-config');

provide(ParentAConfigKey, { apiBase: '/api' });
provide(ParentBConfigKey, { apiBase: '/api/v2' });
  1. 命名空间:
provide('parentA:config', { apiBase: '/api' });
provide('parentB:config', { apiBase: '/api/v2' });

6.4 陷阱:类型推断失败

错误代码:

// 使用字符串 key,无类型信息
provide('theme', ref('dark'));
const theme = inject('theme'); // 类型为 unknown

正确做法:

// 使用 InjectionKey
const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme');
provide(ThemeKey, ref('dark'));
const theme = inject(ThemeKey); // 类型为 Ref<string> | undefined

6.5 陷阱:在异步上下文中调用

错误代码:

import { provide, ref } from 'vue';

export default {
  async setup() {
    const data = await fetchData();
    provide('data', data); // 错误!provide 必须在 setup 同步执行期间调用
  },
};

正确做法:

import { provide, ref, onMounted } from 'vue';

export default {
  setup() {
    const data = ref(null);
    provide('data', data); // 提供 ref
    
    onMounted(async () => {
      data.value = await fetchData(); // 异步更新 ref
    });
  },
};

原理:provide/inject 依赖 getCurrentInstance() 获取当前组件实例,异步上下文中实例可能丢失。

6.6 陷阱:SSR 单例污染

错误代码:

// server.js —— 全局单例
const app = createSSRApp(App);
app.provide('user', ref(null)); // 全局共享

// 处理请求时
app.provides.user.value = currentUser; // 污染!

正确做法:

// 每个请求创建新应用
function createAppForRequest(currentUser) {
  const app = createSSRApp(App);
  app.provide('user', ref(currentUser)); // 请求级隔离
  return app;
}

app.get('*', (req, res) => {
  const app = createAppForRequest(req.user);
  renderToString(app).then(html => res.send(html));
});

6.7 陷阱:循环依赖

错误代码:

// A 组件 provide 一个依赖 B 的值
// B 组件 inject 该值
// 导致 B 在 A 的 provide 之前 inject

// A.vue
import { provide } from 'vue';
import B from './B.vue';

export default {
  setup() {
    provide('data', 'from A');
    return { B };
  },
  template: '<B />',
};

说明:虽然 provide 在 setup 中同步执行,但若 B 在 A 的 setup 完成前尝试 inject,会失败。Vue 3 的渲染顺序保证父组件 setup 先于子组件,所以一般无此问题。但若使用 setup() 返回渲染函数,需注意。

6.8 陷阱:默认值的副作用

错误代码:

// 每次调用都创建新对象,可能产生意外副作用
const config = inject('config', {
  apiBase: '/api',
  features: createDefaultFeatures(), // 每次 inject 都执行 createDefaultFeatures
});

正确做法:

// 使用工厂函数,Vue 内部缓存
const config = inject('config', () => ({
  apiBase: '/api',
  features: createDefaultFeatures(),
}), true);

6.9 陷阱:修改 readonly 注入

错误代码:

// 父组件
provide('state', readonly(reactive({ count: 0 })));

// 子组件
const state = inject('state');
state.count++; // 警告:Set operation on key "count" failed: target is readonly.

正确做法:

// 父组件提供修改方法
const state = reactive({ count: 0 });
provide('state', readonly(state));
provide('increment', () => state.count++);

// 子组件
const state = inject('state');
const increment = inject('increment');
increment(); // 通过方法修改

6.10 最佳实践:封装为 Composable

推荐做法:

// composables/useTheme.ts
import { inject, provide, ref, readonly, type InjectionKey, type Ref } from 'vue';

interface ThemeContext {
  theme: Readonly<Ref<string>>;
  toggleTheme: () => void;
  setTheme: (theme: string) => void;
}

const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme');

export function provideTheme() {
  const theme = ref<'dark' | 'light'>('dark');
  
  function toggleTheme() {
    theme.value = theme.value === 'dark' ? 'light' : 'dark';
  }
  
  function setTheme(newTheme: string) {
    theme.value = newTheme;
  }
  
  const context: ThemeContext = {
    theme: readonly(theme),
    toggleTheme,
    setTheme,
  };
  
  provide(ThemeKey, context);
  return context;
}

export function useTheme(): ThemeContext {
  const context = inject(ThemeKey);
  if (!context) {
    throw new Error('useTheme() must be called within a component that called provideTheme()');
  }
  return context;
}

收益:

  1. 类型安全:InjectionKey 封装在模块内,外部无需感知。
  2. 错误提示:未在 provideTheme 内调用 useTheme 时抛出明确错误。
  3. 封装性:使用者只需调用 provideTheme(提供方)或 useTheme(消费方)。
  4. 可测试性:测试时可单独 mock useTheme。

6.11 最佳实践:使用 readonly 保护状态

// 父组件独占修改权,子组件只读
const state = reactive({ count: 0, list: [] });
provide('state', readonly(state));
provide('actions', {
  increment: () => state.count++,
  addItem: (item) => state.list.push(item),
  reset: () => { state.count = 0; state.list = []; },
});

// 子组件只能读取 state,修改通过 actions
const state = inject('state');
const actions = inject('actions');

6.12 最佳实践:集中管理 InjectionKey

// keys/index.ts —— 集中导出所有 InjectionKey
export { ThemeKey } from './theme';
export { I18nKey } from './i18n';
export { AuthKey } from './auth';
export { FormKey, FormItemKey } from './form';
export { HttpClientKey } from './httpClient';

收益:

  • 避免 Key 散落各处,便于审查。
  • 重命名时一处修改,全应用生效。
  • 便于生成文档与类型检查。

6.13 最佳实践:SSR 请求级隔离

// server.js —— Nuxt 风格的请求级应用
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';

async function render(url, request) {
  const app = createSSRApp(App);
  
  // 请求级 provide
  app.provide('request', {
    url,
    headers: request.headers,
    user: request.user,
    cookies: request.cookies,
  });
  
  app.provide('user', ref(request.user || null));
  
  const html = await renderToString(app);
  return html;
}

6.14 最佳实践:测试 mock

// tests/setup.ts —— 测试中 mock provide
import { provide } from 'vue';
import { ThemeKey } from '../src/keys';

export function withTheme(theme: string = 'dark') {
  return {
    setup() {
      provide(ThemeKey, {
        theme: ref(theme),
        toggleTheme: vi.fn(),
        setTheme: vi.fn(),
      });
    },
    template: '<slot />',
  };
}

// tests/Component.spec.ts
import { mount } from '@vue/test-utils';
import { withTheme } from './setup';

test('renders theme', () => {
  const wrapper = mount(MyComponent, {
    global: {
      components: { ThemeProvider: withTheme('dark') },
      stubs: { ThemeProvider: true },
    },
  });
  expect(wrapper.text()).toContain('dark');
});

7. 工程实践 | Engineering Practice

7.1 项目结构组织

flowchart TD
    T0["src/"]
    T1["keys/                    # InjectionKey 集中管理"]
    T2["index.ts"]
    T3["theme.ts"]
    T4["i18n.ts"]
    T5["auth.ts"]
    T6["form.ts"]
    T7["composables/             # Composable 封装"]
    T8["useTheme.ts"]
    T9["useI18n.ts"]
    T10["useAuth.ts"]
    T11["useForm.ts"]
    T12["plugins/                 # 插件实现"]
    T13["httpClient.ts"]
    T14["i18n.ts"]
    T15["auth.ts"]
    T16["components/"]
    T17["form/                # 组件库内部使用 provide/inject"]
    T18["Form.vue"]
    T19["FormItem.vue"]
    T20["Input.vue"]
    T21["..."]
    T22["main.ts                  # 应用入口,注册插件"]
    T0 --> T1
    T6 --> T7
    T11 --> T12
    T15 --> T16
    T21 --> T22

7.2 Vite 配置

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  define: {
    __DEV__: process.env.NODE_ENV !== 'production',
  },
  resolve: {
    alias: {
      '@': '/src',
    },
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue'],
          'i18n': ['./src/plugins/i18n.ts'],
          'auth': ['./src/plugins/auth.ts'],
        },
      },
    },
  },
});

7.3 TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "jsx": "preserve",
    "jsxImportSource": "vue",
    "types": ["vite/client"],
    "paths": {
      "@/*": ["./src/*"]
    },
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
}

7.4 Vue Devtools 调试

Vue Devtools 6+ 支持 provide/inject 的可视化:

  1. 组件树面板:选中组件,查看其 provide 列表。
  2. Timeline 面板:记录 provide/inject 调用时机。
  3. 自定义 Inspector:插件可注册自定义面板,展示注入关系。

调试技巧:

// 开发模式下记录所有 provide/inject
if (import.meta.env.DEV) {
  const originalProvide = provide;
  const originalInject = inject;
  
  provide = (key, value) => {
    console.debug(`[provide] ${String(key)}`, value);
    return originalProvide(key, value);
  };
  
  inject = (key, defaultValue, treatDefaultAsFactory) => {
    const value = originalInject(key, defaultValue, treatDefaultAsFactory);
    console.debug(`[inject] ${String(key)}`, value);
    return value;
  };
}

7.5 单元测试

// tests/composables/useTheme.spec.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { ref, h, defineComponent } from 'vue';
import { provideTheme, useTheme } from '@/composables/useTheme';

describe('useTheme', () => {
  it('provides and consumes theme', () => {
    const Consumer = defineComponent({
      setup() {
        const { theme, toggleTheme } = useTheme();
        return () => h('div', [
          h('span', theme.value),
          h('button', { onClick: toggleTheme }, 'toggle'),
        ]);
      },
    });

    const Provider = defineComponent({
      setup() {
        provideTheme();
        return () => h(Consumer);
      },
    });

    const wrapper = mount(Provider);
    expect(wrapper.text()).toContain('dark');

    wrapper.find('button').trigger('click');
    expect(wrapper.text()).toContain('light');
  });

  it('throws when used without provider', () => {
    const Consumer = defineComponent({
      setup() {
        expect(() => useTheme()).toThrow('useTheme() must be called within');
        return () => h('div');
      },
    });

    mount(Consumer);
  });

  it('supports custom initial theme', () => {
    const Consumer = defineComponent({
      setup() {
        const { theme } = useTheme();
        return () => h('span', theme.value);
      },
    });

    const Provider = defineComponent({
      setup() {
        provideTheme('light');
        return () => h(Consumer);
      },
    });

    const wrapper = mount(Provider);
    expect(wrapper.text()).toBe('light');
  });
});

7.6 集成测试

// tests/integration/form.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Form from '@/components/form/Form.vue';
import FormItem from '@/components/form/FormItem.vue';
import Input from '@/components/form/Input.vue';

describe('Form integration', () => {
  it('validates form fields', async () => {
    const model = { username: '', email: '' };
    const rules = {
      username: [{ required: true, message: 'Username is required' }],
      email: [
        { required: true, message: 'Email is required' },
        { 
          validator: (value) => /^[^@]+@[^@]+$/.test(value) || Promise.reject('Invalid email'),
          message: 'Invalid email format',
        },
      ],
    };

    const wrapper = mount(Form, {
      props: { model, rules },
      slots: {
        default: `
          <FormItem prop="username" label="Username" :required="true">
            <Input v-model="model.username" />
          </FormItem>
          <FormItem prop="email" label="Email" :required="true">
            <Input v-model="model.email" />
          </FormItem>
        `,
      },
      global: {
        components: { FormItem, Input },
      },
    });

    // 触发校验
    const valid = await wrapper.vm.validate();
    expect(valid).toBe(false);
    
    // 检查错误提示
    expect(wrapper.text()).toContain('Username is required');
    expect(wrapper.text()).toContain('Email is required');
  });
});

7.7 SSR 兼容

// Nuxt 3 风格的请求级状态
// plugins/auth.ts
import { ref, readonly } from 'vue';

export default defineNuxtPlugin((nuxtApp) => {
  const user = ref(null);

  // 服务端获取用户
  if (import.meta.server) {
    nuxtApp.hook('app:created', async () => {
      const headers = useRequestHeaders(['cookie']);
      user.value = await $fetch('/api/me', { headers });
    });
  }

  // 客户端 hydration
  if (import.meta.client) {
    nuxtApp.hook('app:mounted', async () => {
      if (!user.value) {
        user.value = await $fetch('/api/me');
      }
    });
  }

  nuxtApp.provide('auth', {
    user: readonly(user),
    login: async (credentials) => {
      user.value = await $fetch('/api/login', { method: 'POST', body: credentials });
    },
    logout: async () => {
      await $fetch('/api/logout', { method: 'POST' });
      user.value = null;
    },
  });
});

// composables/useAuth.ts
export function useAuth() {
  const { $auth } = useNuxtApp();
  return $auth;
}

7.8 插件开发规范

// plugins/types.ts —— 插件类型定义
import type { App, InjectionKey } from 'vue';

export interface PluginOptions<T> {
  key?: InjectionKey<T>;
  install: (app: App, options?: any) => T;
}

export function definePlugin<T>(options: PluginOptions<T>) {
  return {
    install(app: App, pluginOptions?: any) {
      const value = options.install(app, pluginOptions);
      if (options.key) {
        app.provide(options.key, value);
      }
      return value;
    },
  };
}
typescript
// plugins/logger.ts —— 示例插件
import { definePlugin } from './types';

export interface Logger {
  info: (message: string, ...args: any[]) => void;
  warn: (message: string, ...args: any[]) => void;
  error: (message: string, ...args: any[]) => void;
  debug: (message: string, ...args: any[]) => void;
}

export const LoggerKey: InjectionKey<Logger> = Symbol('logger');

export const loggerPlugin = definePlugin<Logger>({
  key: LoggerKey,
  install(app, options: { level?: 'info' | 'warn' | 'error' | 'debug' } = {}) {
    const level = options.level || 'info';
    const levels = { debug: 0, info: 1, warn: 2, error: 3 };
    
    return {
      info: (message, ...args) => {
        if (levels[level] <= levels.info) {
          console.log(`[INFO] ${message}`, ...args);
        }
      },
      warn: (message, ...args) => {
        if (levels[level] <= levels.warn) {
          console.warn(`[WARN] ${message}`, ...args);
        }
      },
      error: (message, ...args) => {
        if (levels[level] <= levels.error) {
          console.error(`[ERROR] ${message}`, ...args);
        }
      },
      debug: (message, ...args) => {
        if (levels[level] <= levels.debug) {
          console.debug(`[DEBUG] ${message}`, ...args);
        }
      },
    };
  },
});

// main.ts
app.use(loggerPlugin, { level: 'debug' });

// 使用
const logger = inject(LoggerKey);
logger.info('Application started');

7.9 性能监控

// composables/useProvidePerformance.ts
import { getCurrentInstance, onMounted, onUnmounted } from 'vue';

export function useProvidePerformance() {
  if (!import.meta.env.DEV) return;

  const instance = getCurrentInstance();
  if (!instance) return;

  let injectCallCount = 0;
  let totalTime = 0;

  const originalInject = inject;
  
  // 包装 inject 计时(仅开发环境)
  // 注意:实际实现需更复杂,此处仅示意

  onMounted(() => {
    if (injectCallCount > 10) {
      console.warn(
        `[performance] ${injectCallCount} inject calls in <${
          instance.type.name || 'Anonymous'
        }>, total time: ${totalTime}ms`
      );
    }
  });
}

7.10 调试工具

// devtools/provideInspector.ts —— Vue Devtools 自定义 Inspector
import type { App, DevtoolsPluginApi } from '@vue/devtools-api';

export function setupProvideInspector(app: App) {
  if (!import.meta.env.DEV) return;

  app.config.globalProperties.$__provideInspector = {
    getTree() {
      // 遍历组件树,收集 provide/inject 信息
      const tree = collectProvideTree(app._instance);
      return tree;
    },
  };

  // 注册 Devtools Inspector
  if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
    window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('custom-inspector', {
      id: 'provide-inject',
      label: 'Provide/Inject',
      icon: '○',
      tree: () => collectProvideTree(app._instance),
    });
  }
}

function collectProvideTree(instance: any): any {
  if (!instance) return null;
  
  return {
    name: instance.type.name || 'Anonymous',
    provides: Object.keys(instance.provides || {}),
    children: (instance.subTree?.children || []).map(collectProvideTree),
  };
}

8. 案例研究 | Case Studies

8.1 案例一:Element Plus 的 Form 组件

Element Plus 的 Form 组件是 provide/inject 的经典应用:

// Element Plus 源码(简化)
// packages/components/form/src/form.ts
import type { InjectionKey, Ref } from 'vue';

export interface FormContext {
  formItems: FormItemContext[];
  addItem: (item: FormItemContext) => void;
  removeItem: (item: FormItemContext) => void;
  resetFields: () => void;
  clearValidate: () => void;
  validate: () => Promise<boolean>;
  validateField: (prop: string) => Promise<boolean>;
  // ...
}

export const formContextKey: InjectionKey<FormContext> = Symbol('formContext');

// Form.vue
const formItems = [];
const context: FormContext = {
  formItems,
  addItem: (item) => formItems.push(item),
  removeItem: (item) => {
    const index = formItems.indexOf(item);
    if (index > -1) formItems.splice(index, 1);
  },
  // ...
};

provide(formContextKey, context);

设计要点:

  1. 集中管理校验:Form 通过 provide 收集所有 FormItem,统一触发校验。
  2. 生命周期注册:FormItem 在 onMounted 时通过 addItem 注册,onUnmounted 时 removeItem。
  3. 类型安全:formContextKey 使用 InjectionKey<FormContext>,子组件 inject 时获得完整类型。
  4. readonly 保护:FormItem 仅暴露必要方法,内部状态不可直接修改。

8.2 案例二:Vuetify 的主题系统

Vuetify 3 的主题系统通过 provide/inject 实现全局主题切换:

// Vuetify 源码(简化)
// packages/vuetify/src/composables/theme.ts
import type { InjectionKey, Ref } from 'vue';

export interface ThemeInstance {
  global: Ref<ThemeDefinition>;
  current: Ref<ThemeDefinition>;
  themes: Ref<Record<string, ThemeDefinition>>;
  name: Ref<string>;
  isDark: Ref<boolean>;
  setTheme: (name: string) => void;
  toggleTheme: () => void;
}

export const ThemeSymbol: InjectionKey<ThemeInstance> = Symbol.for('vuetify:theme');

// 插件安装
export function createTheme(options: ThemeOptions) {
  const theme = reactive({
    global: shallowRef(options.defaultTheme),
    current: computed(() => theme.global.value),
    // ...
  });

  return {
    install(app) {
      app.provide(ThemeSymbol, theme);
    },
  };
}

// 使用
export function useTheme() {
  const theme = inject(ThemeSymbol);
  if (!theme) throw new Error('Vuetify useTheme() called outside setup');
  return theme;
}

设计要点:

  1. Symbol.for 而非 Symbol:Vuetify 使用 Symbol.for 实现跨应用共享主题(多个 Vuetify 实例共享)。
  2. computed 派生:current 是 global 的 computed,自动响应。
  3. 全局 API:useTheme() 作为 Composable,简化使用。

8.3 案例三:Nuxt 的 Runtime Config

Nuxt 3 通过 provide/inject 实现服务端与客户端共享的运行时配置:

// Nuxt 源码(简化)
// packages/nuxt/src/app/nuxt.ts
import type { InjectionKey } from 'vue';

export interface RuntimeConfig {
  public: {
    apiBase: string;
    [key: string]: any;
  };
  [key: string]: any;
}

export const runtimeConfigKey: InjectionKey<RuntimeConfig> = Symbol('nuxt:runtime-config');

export const useRuntimeConfig = () => {
  return inject(runtimeConfigKey)!;
};

// 服务端入口
async function createNuxtApp(ssrContext) {
  const app = createSSRApp(RootComponent);
  
  // 请求级配置
  const config = await loadRuntimeConfig(ssrContext);
  app.provide(runtimeConfigKey, reactive(config));
  
  return app;
}

设计要点:

  1. SSR 隔离:每个请求创建独立 app,避免配置污染。
  2. 响应式:reactive(config) 允许运行时更新。
  3. 非空断言:inject(key)! 假设 Nuxt 框架保证注入存在。

8.4 案例四:Vue Router 的注入

Vue Router 通过 provide/inject 实现路由信息共享(4.x 与 5.x 一致):

// Vue Router 源码(简化)
// packages/router/src/router.ts
import type { InjectionKey } from 'vue';
import { RouterSymbol } from './injectionSymbols';

export class Router {
  install(app: App) {
    app.provide(RouterSymbol, this);
    app.config.globalProperties.$router = this;
    
    const currentRoute = this.currentRoute;
    app.provide(RouterMatchedKey, currentRoute);
  }
}

// composables/useRouter.ts
export function useRouter() {
  return inject(RouterSymbol)!;
}

export function useRoute() {
  return inject(RouterMatchedKey)!;
}

设计要点:

  1. 类实例注入:Router 是类,通过 provide 注入实例。
  2. 响应式路由:currentRoute 是 ref,路由变化时自动响应。
  3. 全局属性兼容:同时提供 $router、$route 全局属性,兼容 Options API。

8.5 案例五:Pinia 的实现

Pinia 内部也使用 provide/inject 实现插件机制:

// Pinia 源码(简化)
// packages/pinia/src/createPinia.ts
import type { InjectionKey } from 'vue';

export const piniaSymbol: InjectionKey<Pinia> = Symbol('pinia');

export function createPinia() {
  const pinia: Pinia = {
    install(app) {
      app.provide(piniaSymbol, pinia);
      // ...
    },
    state: reactive({}),
    _s: new Map(),
    // ...
  };
  return pinia;
}

export function usePinia() {
  return inject(piniaSymbol)!;
}

8.6 案例六:企业级微前端架构

在微前端架构中,主应用通过 provide/inject 向子应用注入共享服务:

// 主应用
const app = createApp(MainApp);
app.provide('sharedServices', {
  httpClient: axios.create({ baseURL: '/api' }),
  authService: createAuthService(),
  themeService: createThemeService(),
  eventBus: createEventBus(),
});

// 子应用(通过 Vue 插件机制消费)
const subApp = createApp(SubApp);
subApp.config.globalProperties.$sharedServices = mainApp._context.provides.sharedServices;

优势:

  1. 服务共享:主应用统一管理服务,子应用无需重复实现。
  2. 解耦:子应用通过 inject 获取依赖,不直接 import 主应用代码。
  3. 隔离:每个子应用有独立 Vue 实例,状态隔离。

8.7 案例七:VueUse 的 createGlobalState

VueUse 提供的 createGlobalState 是 provide/inject 的应用:

// VueUse 源码(简化)
export function createGlobalState<T>(stateFactory: () => T) {
  let initialized = false;
  let state: T;
  const scope = effectScope(true);

  return () => {
    if (!initialized) {
      state = scope.run(stateFactory)!;
      initialized = true;
    }
    return state;
  };
}

// 使用
const useCounter = createGlobalState(() => {
  const count = ref(0);
  function increment() {
    count.value++;
  }
  return { count, increment };
});

// 任意组件
const { count, increment } = useCounter();

设计要点:

  1. effectScope 隔离:通过 effectScope 创建独立作用域,避免响应式副作用泄漏。
  2. 单例模式:首次调用创建状态,后续调用返回同一实例。
  3. 替代 provide/inject:适用于不需要组件树关系的全局状态。

填空题知识点讲解

题目 1:provide 在组件实例上的内部存储属性名为 ______,子组件通过 ______ 链向上查找。

解析讲解:provides,原型链(prototype chain)

解析讲解:Vue 3 内部每个组件实例有 provides 对象,子组件的 provides 通过 Object.create(parent.provides) 创建,原型指向父组件的 provides,形成原型链。


题目 2:InjectionKey<T> 是 ______ 的子类型,用于实现类型安全的依赖注入。

解析讲解:Symbol

解析讲解:InjectionKey<T> 在 TypeScript 中声明为 interface InjectionKey<T> extends Symbol {},运行时是一个 Symbol,编译时携带泛型类型信息 T。


题目 3:inject 的查找复杂度为 ______,其中 d 是组件树深度。

解析讲解:O(d)O(d)

解析讲解:inject 沿祖先链向上查找,最坏情况需遍历从当前组件到根的所有祖先,复杂度为 O(d)O(d),其中 dd 是组件树深度。


题目 4:Vue 3 中 provide 的值若为 ______ 或 ______,则 inject 返回的也是响应式引用。

解析讲解:ref,reactive

解析讲解:Vue 3 的响应式系统基于 Proxy,ref 和 reactive 创建的对象自动响应式。provide 传递这些对象的引用,inject 返回同一引用,保持响应性。


题目 5:在 SSR 中避免 provide 单例污染的方法是创建 ______ 应用实例。

解析讲解:请求级

解析讲解:每个 HTTP 请求创建独立的 Vue 应用实例,通过 app.provide() 注入请求相关的数据(如用户信息),避免不同请求间共享状态。


编程题知识点讲解

题目 1:实现一个类型安全的 useCounter Composable,使用 provide/inject 在组件树中共享计数器状态,支持 increment、decrement、reset 操作,并保证子组件只读访问 count。

// 参考答案
import { ref, readonly, provide, inject, type InjectionKey, type Readonly, type Ref } from 'vue';

interface CounterContext {
  count: Readonly<Ref<number>>;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const CounterKey: InjectionKey<CounterContext> = Symbol('counter');

export function provideCounter(initialValue: number = 0) {
  const count = ref(initialValue);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  function reset() {
    count.value = initialValue;
  }

  const context: CounterContext = {
    count: readonly(count),
    increment,
    decrement,
    reset,
  };

  provide(CounterKey, context);
  return context;
}

export function useCounter(): CounterContext {
  const context = inject(CounterKey);
  if (!context) {
    throw new Error('useCounter() must be called within a component that called provideCounter()');
  }
  return context;
}

题目 2:实现一个 useModal Composable,通过 provide/inject 管理模态框的打开/关闭状态,支持多个模态框独立控制。

// 参考答案
import { ref, reactive, provide, inject, readonly, type InjectionKey } from 'vue';

interface ModalState {
  isOpen: boolean;
  open: () => void;
  close: () => void;
  toggle: () => void;
}

interface ModalManager {
  modals: Record<string, ModalState>;
  register: (id: string) => ModalState;
  unregister: (id: string) => void;
  getModal: (id: string) => ModalState | undefined;
}

const ModalManagerKey: InjectionKey<ModalManager> = Symbol('modal-manager');

export function provideModalManager() {
  const modals = reactive<Record<string, ModalState>>({});

  function register(id: string): ModalState {
    if (modals[id]) {
      return modals[id];
    }

    const isOpen = ref(false);
    const modal: ModalState = {
      get isOpen() {
        return isOpen.value;
      },
      set isOpen(value) {
        isOpen.value = value;
      },
      open: () => {
        isOpen.value = true;
      },
      close: () => {
        isOpen.value = false;
      },
      toggle: () => {
        isOpen.value = !isOpen.value;
      },
    };

    modals[id] = modal;
    return modal;
  }

  function unregister(id: string) {
    delete modals[id];
  }

  function getModal(id: string): ModalState | undefined {
    return modals[id];
  }

  const manager: ModalManager = {
    modals: readonly(modals),
    register,
    unregister,
    getModal,
  };

  provide(ModalManagerKey, manager);
  return manager;
}

export function useModalManager(): ModalManager {
  const manager = inject(ModalManagerKey);
  if (!manager) {
    throw new Error('useModalManager() must be called within a component that called provideModalManager()');
  }
  return manager;
}

export function useModal(id: string): ModalState {
  const manager = useModalManager();
  return manager.register(id);
}

题目 3:实现一个 usePermission Composable,结合 provide/inject 与用户认证状态,实现基于角色的权限控制(RBAC)。

// 参考答案
import { computed, inject, provide, readonly, type ComputedRef, type InjectionKey, type Ref } from 'vue';

interface User {
  id: string;
  name: string;
  roles: string[];
  permissions: string[];
}

interface AuthContext {
  user: Readonly<Ref<User | null>>;
  isAuthenticated: ComputedRef<boolean>;
  login: (credentials: { email: string; password: string }) => Promise<void>;
  logout: () => Promise<void>;
}

const AuthKey: InjectionKey<AuthContext> = Symbol('auth');

export function provideAuth() {
  const user = ref<User | null>(null);

  const isAuthenticated = computed(() => user.value !== null);

  async function login(credentials: { email: string; password: string }) {
    // 实际实现调用 API
    user.value = {
      id: '1',
      name: 'Admin',
      email: credentials.email,
      roles: ['admin'],
      permissions: ['read', 'write', 'delete'],
    };
  }

  async function logout() {
    user.value = null;
  }

  const context: AuthContext = {
    user: readonly(user),
    isAuthenticated,
    login,
    logout,
  };

  provide(AuthKey, context);
  return context;
}

export function useAuth(): AuthContext {
  const context = inject(AuthKey);
  if (!context) {
    throw new Error('useAuth() must be called within a component that called provideAuth()');
  }
  return context;
}

export function usePermission() {
  const { user } = useAuth();

  function hasRole(role: string): boolean {
    return user.value?.roles.includes(role) ?? false;
  }

  function hasPermission(permission: string): boolean {
    return user.value?.permissions.includes(permission) ?? false;
  }

  function hasAnyRole(roles: string[]): boolean {
    return roles.some(hasRole);
  }

  function hasAllRoles(roles: string[]): boolean {
    return roles.every(hasRole);
  }

  function hasAnyPermission(permissions: string[]): boolean {
    return permissions.some(hasPermission);
  }

  function hasAllPermissions(permissions: string[]): boolean {
    return permissions.every(hasPermission);
  }

  return {
    hasRole,
    hasPermission,
    hasAnyRole,
    hasAllRoles,
    hasAnyPermission,
    hasAllPermissions,
  };
}

// 指令:v-permission="'write'"
export const vPermission = {
  mounted(el: HTMLElement, binding: { value: string | string[] }) {
    const { hasPermission, hasAnyPermission } = usePermission();
    const permissions = Array.isArray(binding.value) ? binding.value : [binding.value];
    
    if (!hasAnyPermission(permissions)) {
      el.parentNode?.removeChild(el);
    }
  },
};

// 组件:<RequirePermission permission="write">...</RequirePermission>
export const RequirePermission = defineComponent({
  props: {
    permission: {
      type: [String, Array] as PropType<string | string[]>,
      required: true,
    },
    fallback: {
      type: String,
      default: '',
    },
  },
  setup(props, { slots }) {
    const { hasAnyPermission } = usePermission();
    const permissions = computed(() =>
      Array.isArray(props.permission) ? props.permission : [props.permission]
    );

    return () =>
      hasAnyPermission(permissions.value)
        ? slots.default?.()
        : props.fallback || null;
  },
});

10.1 官方文档

[1] Evan You and the Vue.js Team. 2024. Vue.js 3 Official Documentation: Component Basics. Retrieved July 20, 2026 from https://vuejs.org/guide/components/provide-inject.html

[2] Evan You and the Vue.js Team. 2024. Vue.js 3 Official Documentation: Reactivity Fundamentals. Retrieved July 20, 2026 from https://vuejs.org/guide/essentials/reactivity-fundamentals.html

[3] Evan You and the Vue.js Team. 2024. Vue.js 3 Official Documentation: Composition API. Retrieved July 20, 2026 from https://vuejs.org/guide/extras/composition-api-faq.html

[4] Evan You and the Vue.js Team. 2024. Vue.js 3 API Reference: provide. Retrieved July 20, 2026 from https://vuejs.org/api/composition-api-dependency-injection.html#provide

[5] Evan You and the Vue.js Team. 2024. Vue.js 3 API Reference: inject. Retrieved July 20, 2026 from https://vuejs.org/api/composition-api-dependency-injection.html#inject

10.2 学术文献

[6] Martin Fowler. 2004. Inversion of Control Containers and the Dependency Injection Pattern. Retrieved July 20, 2026 from https://martinfowler.com/articles/injection.html

[7] Robert C. Martin. 2003. Agile Software Development, Principles, Patterns, and Practices. Prentice Hall, Upper Saddle River, NJ, USA.

[8] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. 1994. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional, Boston, MA, USA.

[9] Evan You. 2020. Vue 3.0 Released. Retrieved July 20, 2026 from https://blog.vuejs.org/posts/vue-3-one-piece

[10] Evan You. 2021. Vue 3.2 Released. Retrieved July 20, 2026 from https://blog.vuejs.org/posts/vue-3.2

10.3 相关框架文档

[11] Meta Platforms, Inc. 2024. React Documentation: Context. Retrieved July 20, 2026 from https://react.dev/reference/react/createContext

[12] Google LLC. 2024. Angular Documentation: Dependency Injection. Retrieved July 20, 2026 from https://angular.dev/guide/di

[13] Svelte Foundation. 2024. Svelte Documentation: Context API. Retrieved July 20, 2026 from https://svelte.dev/docs/svelte/context

[14] Pinia Team. 2024. Pinia Documentation. Retrieved July 20, 2026 from https://pinia.vuejs.org/

[15] Nuxt Labs. 2024. Nuxt 3 Documentation: Composables. Retrieved July 20, 2026 from https://nuxt.com/docs/api/composables/use-nuxt-app

10.4 技术专著

[16] Evan You. 2023. Vue.js 3 Design and Implementation (Vue.js 设计与实现). People’s Posts and Telecommunications Press, Beijing, China.

[17] Thiago Delgado Pinto. 2022. Vue.js 3 By Example: Build eight real-world applications from the ground up. Packt Publishing, Birmingham, UK.

[18] Holt Calhoun, Daniel Fallman, and Constantine Lignos. 2023. Vue.js 3 Cookbook: Discover effective techniques to leverage the benefits of Vue 3. Packt Publishing, Birmingham, UK.

10.5 论文与技术报告

[19] Evan You. 2019. Vue 3.0 RFC: Composition API. Retrieved July 20, 2026 from https://github.com/vuejs/rfcs/blob/master/active-rfcs/0000-reactivity-ref-sugar.md

[20] Linus Borg. 2021. A Deep Dive into Vue’s Reactivity System. Retrieved July 20, 2026 from https://vuejs.org/guide/extras/reactivity-in-depth.html

[21] Anthony Fu. 2022. Vue Use: Collection of Essential Vue Composition Utilities. Retrieved July 20, 2026 from https://vueuse.org/

11.1 书籍

  1. 《Vue.js 设计与实现》——霍春阳

    • 深入剖析 Vue 3 响应式系统、组件化、编译优化的实现原理。
    • 包含 provide/inject 的源码级解析。
  2. 《Design Patterns: Elements of Reusable Object-Oriented Software》——Erich Gamma 等

    • 设计模式经典,包含依赖注入、控制反转的理论基础。
  3. 《Agile Software Development, Principles, Patterns, and Practices》——Robert C. Martin

    • 详细阐述依赖注入、单一职责等原则。
  4. 《Vue.js 3 By Example》——Thiago Delgado Pinto

    • 通过 8 个实战项目讲解 Vue 3,包含 provide/inject 的实际应用。
  5. 《Composition API with Vue 3》——Daniel Klotz

    • 专注于 Composition API,深入探讨 provide/inject 的最佳实践。

11.2 论文与 RFC

  1. Vue 3 Reactivity RFC:https://github.com/vuejs/rfcs

    • Vue 官方的 RFC 列表,包含响应式系统、provide/inject 的设计讨论。
  2. Vue 3 Source Code:https://github.com/vuejs/core

    • Vue 3 源码,重点关注 packages/runtime-core/src/apiInject.ts。
  3. Inversion of Control Containers and the Dependency Injection Pattern——Martin Fowler

    • 依赖注入模式的奠基性文章,阐述 IoC 与 DI 的本质。

11.5 社区与讨论

  1. Vue Discord:https://discord.com/invite/vue

    • Vue 官方 Discord,与社区讨论 provide/inject 的实践。
  2. Vue Forum:https://forum.vuejs.org/

    • Vue 官方论坛,搜索 provide/inject 标签查找历史讨论。
  3. Vue RFC Discussions:https://github.com/vuejs/rfcs/discussions

    • Vue RFC 讨论,参与 provide/inject 的未来演进。
  4. Reddit r/vuejs:https://www.reddit.com/r/vuejs/

    • Vue 社区,分享 provide/inject 的使用经验。
  5. Stack Overflow:https://stackoverflow.com/questions/tagged/vue.js

    • 技术问答,搜索 provide/inject 相关问题与解答。

附录 A:provide/inject API 速查

A.1 provide

function provide<T>(key: InjectionKey<T> | string | number, value: T): void

参数:

  • key:注入键,可以是 InjectionKey<T>、string 或 number。
  • value:注入值,可以是任意类型(ref、reactive、普通对象、函数等)。

返回值:无。

调用时机:必须在 setup() 同步执行期间调用。

A.2 inject

function inject<T>(key: InjectionKey<T> | string): T | undefined
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
function inject<T>(key: InjectionKey<T> | string, defaultValue: () => T, treatDefaultAsFactory: true): T

参数:

  • key:注入键。
  • defaultValue(可选):未找到时的默认值。
  • treatDefaultAsFactory(可选):是否将 defaultValue 视为工厂函数。

返回值:注入值或默认值。

A.3 app.provide

app.provide<T>(key: InjectionKey<T> | string | number, value: T): void

说明:应用级 provide,所有组件均可通过 inject 访问。

A.4 readonly

function readonly<T extends object>(target: T): Readonly<T>

说明:返回只读 Proxy,拦截 set 操作并警告。

A.5 InjectionKey

interface InjectionKey<T> extends Symbol {}

说明:Symbol 的子类型,携带泛型类型信息 T。


附录 B:常见错误信息

B.1 inject() can only be used inside setup() or functional components

原因:inject 在异步上下文或非 setup 函数中调用。

解决:确保 inject 在 setup() 同步执行期间调用,使用 ref 保存值后在异步回调中使用。

B.2 useXxx() must be called within a component that called provideXxx()

原因:未在父组件调用 provideXxx 即在子组件使用 useXxx。

解决:在组件树的祖先组件中调用 provideXxx。

B.3 Set operation on key “xxx” failed: target is readonly

原因:尝试修改 readonly() 包装的注入值。

解决:通过父组件提供的修改方法间接修改,或移除 readonly() 包装。

B.4 Hydration node mismatch

原因:SSR 中 provide 的值在服务端与客户端不一致。

解决:确保服务端与客户端 provide 相同初始值,动态值通过 onMounted 等客户端钩子更新。


附录 C:版本兼容性

Vue 版本provide/inject 特性
2.2首次引入,非响应式
2.7支持 Composition API,响应式
3.0完全重构,InjectionKey 类型安全
3.2SSR 优化,应用级 provide 改进
3.3实验性特性,无重大变化
3.4性能优化,内部实现改进
3.5稳定性提升,无 API 变化

升级建议:

  • Vue 2 项目升级:provide/inject 在 Vue 2.7+ 支持 Composition API,可平滑迁移。
  • Vue 3 项目:建议使用 InjectionKey<T> 保证类型安全。
  • SSR 项目:Vue 3.2+ 优化了 SSR 场景,推荐升级。

结语

provide/inject 是 Vue 3 中实现依赖注入的核心机制,适用于跨层级通信、组件库内部协作、插件服务注入等场景。本章节从历史动机、形式化定义、原理推导、代码示例、对比分析、最佳实践、工程实践、案例研究、习题等维度,系统化阐述了 provide/inject 的设计哲学与工程应用。

核心要点回顾:

  1. 依赖注入模式:provide/inject 实现了控制反转,父组件控制子组件依赖的提供。
  2. 响应式注入:传递 ref/reactive 自动响应式,子组件依赖属性级追踪。
  3. 类型安全:InjectionKey<T> 是 Symbol 的子类型,实现运行时键与编译时类型的统一。
  4. 原型链查找:内部通过 Object.create(parent.provides) 实现链式查找,复杂度 O(d)O(d)。
  5. SSR 注意:app.provide() 是应用级单例,需通过请求级应用实例避免污染。
  6. 最佳实践:封装为 Composable、使用 readonly 保护状态、集中管理 InjectionKey。
  7. 适用场景:组件库内部协作、主题/国际化、插件服务注入、SSR 请求级状态。

掌握 provide/inject 的原理与最佳实践,是构建大型 Vue 应用的关键能力。在实际项目中,应根据场景灵活选择 provide/inject、props/emits、Pinia 等通信方式,避免过度使用或误用。

基础用法

provide 提供依赖 provide(<key>, <value>);

import { provide, ref } from 'vue';

// 字符串 key
provide('theme', 'dark');

// Symbol key(推荐)
provide(Symbol('user'), { name: 'Tom' });

// 注入响应式值
const count = ref(0);
provide('count', count);

// 提供方法
provide('increment', () => count.value++);

inject 注入依赖 const <value> = inject(<key>, [defaultValue], [treatDefaultAsFactory]);

import { inject } from 'vue';

// 注入(可能为 undefined)
const theme = inject('theme');

// 注入带默认值
const theme = inject('theme', 'light');

// 注入工厂函数作为默认值
const config = inject('config', () => createDefaultConfig(), true);

响应式 provide/inject

提供响应式状态

import { provide, ref, readonly } from 'vue';

const theme = ref('dark');
const toggleTheme = () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark';
};

// 提供只读状态 + 修改方法(单向数据流)
provide('theme', readonly(theme));
provide('toggleTheme', toggleTheme);

子组件使用

import { inject } from 'vue';

const theme = inject<Readonly<Ref<string>>>('theme');
const toggleTheme = inject<() => void>('toggleTheme');

// theme.value 是只读,只能通过 toggleTheme 修改

完整 store 模式

import { provide, ref, readonly, computed } from 'vue';

function provideUserStore() {
  const user = ref<{ id: number; name: string } | null>(null);
  const isLoading = ref(false);
  const isLoggedIn = computed(() => !!user.value);

  async function login(name: string) {
    isLoading.value = true;
    user.value = await fetchUser(name);
    isLoading.value = false;
  }

  function logout() {
    user.value = null;
  }

  provide('userStore', {
    user: readonly(user),
    isLoading: readonly(isLoading),
    isLoggedIn,
    login,
    logout
  });
}

类型安全

InjectionKey 类型化注入 const <key> = Symbol() as InjectionKey<Type>;

import type { InjectionKey, Ref } from 'vue';
import { provide, inject } from 'vue';

interface UserContext {
  user: Ref<{ id: number; name: string } | null>;
  login: (name: string) => Promise<void>;
  logout: () => void;
}

export const UserKey: InjectionKey<UserContext> = Symbol('UserContext');

// 父组件
provide(UserKey, {
  user: ref(null),
  login: async (name) => { /* ... */ },
  logout: () => { /* ... */ }
});

// 子组件(自动推断类型)
const userStore = inject(UserKey);
if (userStore) {
  userStore.login('Tom');  // 类型安全
}

Symbol 共享 key

// keys.ts
import type { InjectionKey } from 'vue';
export const ThemeKey: InjectionKey<string> = Symbol('theme');
export const ApiKey: InjectionKey<{ base: string }> = Symbol('api');

// provider.vue
import { provide } from 'vue';
import { ThemeKey, ApiKey } from './keys';
provide(ThemeKey, 'dark');
provide(ApiKey, { base: '/api/v1' });

// consumer.vue
import { inject } from 'vue';
import { ThemeKey, ApiKey } from './keys';
const theme = inject(ThemeKey);  // string | undefined
const api = inject(ApiKey);      // { base: string } | undefined

provide 应用级

app.provide 全局提供 app.provide(<key>, <value>);

import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);
app.provide('apiBase', import.meta.env.VITE_API_BASE);
app.provide('appName', 'FANDEX');
app.mount('#app');

全局 config 注入

app.provide('config', {
  apiBase: '/api',
  cdnBase: 'https://cdn.example.com',
  timeout: 30000
});

// 任意组件
const config = inject('config');

默认值与工厂

静态默认值

const theme = inject('theme', 'light');
const timeout = inject('timeout', 3000);

工厂函数默认值 inject(<key>, <factory>, true);

// 第三个参数 true 表示第二个参数是工厂函数
const store = inject('store', () => createStore(), true);
const config = inject('config', () => ({}), true);

注入的响应性

保持响应性(提供 ref)

import { provide, ref, inject } from 'vue';

// 父
const count = ref(0);
provide('count', count);

// 子(任意层级)
const count = inject<Ref<number>>('count');
count.value++;  // 修改会反映到所有注入处

保持响应性(提供 reactive)

import { provide, reactive, inject } from 'vue';

// 父
const state = reactive({ count: 0 });
provide('state', state);

// 子
const state = inject<typeof state>('state');
state.count++;

provide/inject 调试

getCurrentInstance 查看注入链

import { getCurrentInstance } from 'vue';

const instance = getCurrentInstance();
const provides = instance?.provides;
console.log(provides);

useContext 模式

import { inject, provide, type InjectionKey } from 'vue';

export function createContext<T>(name: string) {
  const key: InjectionKey<T> = Symbol(name);

  const provideContext = (value: T) => provide(key, value);
  const useContext = (defaultValue?: T) => inject(key, defaultValue);

  return { provideContext, useContext, key };
}

// 使用
const { provideContext, useContext } = createContext<{ user: string }>('User');
provideContext({ user: 'Tom' });
const ctx = useContext();

完整示例

主题切换 Provider

import { provide, inject, ref, readonly, computed, type InjectionKey, type Ref } from 'vue';

interface ThemeContext {
  theme: Readonly<Ref<'dark' | 'light'>>;
  isDark: Ref<boolean>;
  toggleTheme: () => void;
}

export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme');

export function useThemeProvider() {
  const theme = ref<'dark' | 'light'>('dark');
  const isDark = computed(() => theme.value === 'dark');

  function toggleTheme() {
    theme.value = theme.value === 'dark' ? 'light' : 'dark';
  }

  const context: ThemeContext = {
    theme: readonly(theme),
    isDark,
    toggleTheme
  };

  provide(ThemeKey, context);
  return context;
}

export function useTheme() {
  const ctx = inject(ThemeKey);
  if (!ctx) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用');
  }
  return ctx;
}

子组件消费

import { useTheme } from './theme';

const { theme, isDark, toggleTheme } = useTheme();
console.log(theme.value);    // 'dark' 或 'light'
console.log(isDark.value);   // true / false
toggleTheme();