前置知识: Vue 3

Provide与Inject

84 minIntermediate2026/6/14

依赖注入与跨层级通信

Provide 与 Inject | Dependency Injection in Vue 3

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


目录

  1. 学习目标
  2. 历史动机与发展脉络
  3. 形式化定义
  4. 理论推导与原理解析
  5. 代码示例
  6. 对比分析
  7. 常见陷阱与最佳实践
  8. 工程实践
  9. 案例研究
  10. 习题
  11. 参考文献
  12. 延伸阅读

1. 学习目标 | Learning Objectives

本章节基于 Bloom 教育目标分类法设计学习目标,覆盖记忆、理解、应用、分析、评价、创造六个层次。完成本章学习后,学习者应能够独立设计企业级依赖注入架构,并对其适用场景与限制做出准确判断。

1.1 记忆层(Remember)

  • R1:准确陈述 Vue 3 中 provideinject 两个 API 的函数签名,包括 provide(key, value)inject(key, defaultValue?, treatDefaultAsFactory?)
  • R2:列举 provide/inject 的三种典型使用场景:跨层级通信、插件配置注入、组件库主题传递。
  • R3:复述 InjectionKey<T> 的类型定义:它是 Symbol 的子类型,泛型参数 T 描述注入值的类型。
  • R4:背记 provide/inject 必须在 setup() 同步执行期间调用,不能放在异步回调或 onMounted 等生命周期钩子中调用。
  • R5:识别 provide 的注入查找规则:子组件 inject 时,沿组件树向上查找最近的 provide,直到根组件或未找到则使用默认值。

1.2 理解层(Understand)

  • U1:解释依赖注入(Dependency Injection, DI)模式的核心思想:控制反转(Inversion of Control, IoC),由父组件控制子组件依赖的提供,而非子组件主动获取。
  • U2:阐述 Vue 3 的 provide/inject 与 Vue 2 的关键差异:Vue 2 的 provide 是非响应式的(除非主动返回响应式对象),Vue 3 完全整合 Composition API,可直接传递 ref/reactive 实现响应式注入。
  • U3:描述 provide 在组件实例上的内部存储结构:每个组件实例有 provides 选项,子组件的 provides 通过原型链指向父组件的 provides,形成链式查找。
  • U4:理解 InjectionKey<T> 的类型安全机制:通过 Symbol 作为运行时键、泛型 T 作为编译时类型,实现类型与值的统一。
  • U5:说明 provide/inject 与 Pinia/Vuex 的本质区别:前者是组件树内的依赖注入,后者是全局状态管理,二者适用场景不同。

1.3 应用层(Apply)

  • A1:使用 provide/inject 实现一个完整的主题切换系统,支持亮色/暗色双主题,并保证响应式更新。
  • A2:使用 InjectionKey<T> 实现类型安全的国际化(i18n)注入,包含语言切换、翻译函数、locale 状态。
  • A3:实现一个表单组件库,通过 provide/injectFormFormItemInput 三层组件间传递校验状态、错误信息、字段名。
  • A4:在 Vue 3 插件开发中,使用 app.provide() 向全局注入服务(如 HTTP 客户端、日志服务、配置对象)。
  • A5:实现一个响应式的用户认证状态注入,支持登录、登出、权限校验,并在多个组件中共享。

1.4 分析层(Analyze)

  • An1:分析 Vue 3 provide/inject 的查找算法复杂度,对比与 React Context 的实现差异(Vue 原型链查找 vs React Provider 查找)。
  • An2:解构 provide 在 SSR 场景下的单例污染问题:app.provide() 在 SSR 中是全局单例,所有请求共享同一份注入,需通过 Symbol 或请求级实例隔离。
  • An3:分析响应式注入的依赖追踪机制:当 inject 的值是 ref 时,组件渲染期间自动建立依赖,ref.value 变化触发组件重新渲染。
  • An4:对比 provide/injectprops/emits、EventBus、Pinia 四种组件通信方式的时间复杂度与空间复杂度。
  • An5:分析组件库(如 Element Plus、Vuetify)如何利用 provide/inject 实现配置全局化与组件间协作。

1.5 评价层(Evaluate)

  • E1:评估一个具体业务场景应当使用 provide/inject 还是 Pinia,权衡开发成本、可维护性、可测试性、可扩展性。
  • E2:判断何时应当将 provide 包装为 Composable(如 useTheme()useI18n()),而非直接在子组件中 inject,权衡 API 友好度与封装成本。
  • E3:评价 readonly() 包装注入值的必要性:何时应当暴露可变状态,何时应当只允许子组件读取、由父组件独占修改权。
  • E4:权衡 provide/inject 在测试中的可维护性:单元测试需要 mock 注入值,对比直接 import 服务的测试复杂度。

1.6 创造层(Create)

  • C1:设计一套企业级依赖注入框架,支持依赖声明、生命周期管理、作用域隔离(单例、请求、组件级),并兼容 Vue 3 的响应式系统。
  • C2:构建一个基于 provide/inject 的多租户 SaaS 架构,支持租户配置注入、权限隔离、主题定制。
  • C3:设计一个 Vue 3 插件体系,使用 app.provide() 注入核心服务,允许第三方插件扩展或替换服务实现。
  • C4:实现一个支持热重载(HMR)的 provide/inject 调试工具,可视化组件树的注入关系,帮助开发者快速定位注入缺失或冲突。

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

2.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 容器设计。

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

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

  1. 跨层级通信需求:组件库中 FormFormItemInput 的多层嵌套,使用 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 引用
    };
  },
};

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

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

2.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 };
  },
};

2.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

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

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

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

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

2.3.4 应用级 provide(Vue 3.0)

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

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

2.3.5 SSR 友好(Vue 3.2+)

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

2.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,则自动响应式,无需额外处理。

2.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 优化。

2.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 仅作为补充。


3. 形式化定义 | Formal Definitions

3.1 组件树的形式化定义

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

  • VV 是组件实例的集合,根组件 rVr \in V
  • EV×VE \subseteq V \times V 是父子关系,(p,c)E(p, c) \in E 表示 ccpp 的子组件。
  • vV{r},!pV:(p,v)E\forall v \in V \setminus \{r\}, \exists! p \in V: (p, v) \in E(每个非根组件有唯一父组件)。

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

ancestors(v)={[]if v=r[p]ancestors(p)if vr,(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}

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

根组件 rrprovidesr\text{provides}_r 通过原型链继承自 appProvides\text{appProvides}

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

3.3 inject 的形式化定义

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

inject(v,k)={providesu[k]if uancestors(v){v}:kprovidesudefaultotherwise\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 的所有祖先。

3.4 响应式注入的形式化

定义 3.7(响应式注入值):若 provide 的值 val\text{val} 是响应式对象(refreactive),则 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} 变化时,所有依赖该值的组件触发重渲染。

3.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:返回值自动推断为 TundefinedT | \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):Tundefined\forall k: \text{InjectionKey}<T>, \forall c: \text{consumer}: \text{inject}(c, k): T | \text{undefined}

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

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

3.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)}

3.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 或默认值。


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

4.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 注入的值可被全应用访问。

4.2 响应式注入的依赖追踪

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

响应式注入的执行流程

  1. 组件渲染setup() 执行 inject('theme'),返回 Ref<string>
  2. 模板求值:模板中 {{ theme }} 触发 theme.valueget,进入 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 变化时:

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

4.3 inject 的查找复杂度分析

设组件树深度为 dd,键 kk 在第 ii 层(0id0 \leq i \leq d)被 providei=0i=0 表示根组件),则 inject 的查找步数为 did - i

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

E[steps]=1d+1i=0d(di)=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

4.4 与 React Context 的性能对比

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

  • 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 个消费者,每个消费者依赖 valuekk 个属性中的 1 个。

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

4.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 的最佳实践。

4.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. 定义 Keyconst 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 即导出类型契约。

4.7 readonly 注入的拦截机制

readonly() 返回一个 Proxy,拦截 setdelete 操作:

// 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. 调试友好:若子组件尝试修改,控制台立即警告,便于定位问题。

5. 代码示例 | Code Examples

5.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>
<!-- 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>

5.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');
// 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;
}
<!-- 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>

5.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>
<!-- 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>
<!-- 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>

5.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');
<!-- 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>
<!-- 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>

5.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');
// 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;
}
// 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');
<!-- 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>

5.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');
// 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;
}

5.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;
}

5.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 调用都会执行工厂函数,返回独立的默认配置
// 适用于需要隔离状态的场景

5.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');

5.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'}>`
  );
}

6. 对比分析 | Comparative Analysis

6.1 与 Props/Emit 的对比

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

选择建议

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

6.2 与 EventBus 的对比

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

EventBus 的问题

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

6.3 与 Pinia/Vuex 的对比

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

选择建议

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

6.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 时声明,后者更集中。

6.5 与 Angular DI 的对比

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

Angular DI 的优势

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

Vue 的设计哲学

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

6.6 与 Svelte Context 的对比

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

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

6.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

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

7.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,则传递的是字符串值,失去响应式。

7.2 陷阱:解构失去响应式

错误代码

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

正确做法

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

7.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' });

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

7.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() 获取当前组件实例,异步上下文中实例可能丢失。

7.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));
});

7.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 />',
};

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

7.8 陷阱:默认值的副作用

错误代码

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

正确做法

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

7.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(); // 通过方法修改

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

7.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');

7.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 散落各处,便于审查。
  • 重命名时一处修改,全应用生效。
  • 便于生成文档与类型检查。

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

7.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');
});

8. 工程实践 | Engineering Practice

8.1 项目结构组织

src/
├── keys/                    # InjectionKey 集中管理
│   ├── index.ts
│   ├── theme.ts
│   ├── i18n.ts
│   ├── auth.ts
│   └── form.ts
├── composables/             # Composable 封装
│   ├── useTheme.ts
│   ├── useI18n.ts
│   ├── useAuth.ts
│   └── useForm.ts
├── plugins/                 # 插件实现
│   ├── httpClient.ts
│   ├── i18n.ts
│   └── auth.ts
├── components/
│   ├── form/                # 组件库内部使用 provide/inject
│   │   ├── Form.vue
│   │   ├── FormItem.vue
│   │   └── Input.vue
│   └── ...
└── main.ts                  # 应用入口,注册插件

8.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'],
        },
      },
    },
  },
});

8.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"]
}

8.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;
  };
}

8.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');
  });
});

8.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');
  });
});

8.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;
}

8.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;
    },
  };
}
// 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');

8.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`
      );
    }
  });
}

8.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),
  };
}

9. 案例研究 | Case Studies

9.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. 生命周期注册FormItemonMounted 时通过 addItem 注册,onUnmountedremoveItem
  3. 类型安全formContextKey 使用 InjectionKey<FormContext>,子组件 inject 时获得完整类型。
  4. readonly 保护FormItem 仅暴露必要方法,内部状态不可直接修改。

9.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 派生currentglobal 的 computed,自动响应。
  3. 全局 APIuseTheme() 作为 Composable,简化使用。

9.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 框架保证注入存在。

9.4 案例四:Vue Router 的注入

Vue Router 4 通过 provide/inject 实现路由信息共享:

// 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. 响应式路由currentRouteref,路由变化时自动响应。
  3. 全局属性兼容:同时提供 $router$route 全局属性,兼容 Options API。

9.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)!;
}

9.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 实例,状态隔离。

9.7 案例七:VueUse 的 createGlobalState

VueUse 提供的 createGlobalStateprovide/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:适用于不需要组件树关系的全局状态。

10. 习题 | Exercises

10.1 选择题

题目 1:下列关于 Vue 3 provide/inject 的描述,哪一项是错误的?

A. provide 必须在 setup() 同步执行期间调用。 B. inject 沿组件树向上查找,返回最近的 provide。 C. provide 的值若为 ref,则自动响应式。 D. provide/inject 可在任意异步函数中调用。

答案:D

解析provide/inject 依赖 getCurrentInstance() 获取当前组件实例,必须在 setup() 同步执行期间调用。在异步回调(如 setTimeoutawait 之后)中调用,getCurrentInstance() 返回 null,导致 provide/inject 失效。


题目 2:使用 InjectionKey<T> 的主要目的是什么?

A. 提升运行时性能。 B. 实现类型安全的依赖注入。 C. 替代 Symbol 作为注入键。 D. 支持 SSR 隔离。

答案:B

解析InjectionKey<T>Symbol 的子类型,泛型参数 T 用于编译时类型检查。provide(key, value) 时编译器检查 value 是否符合 Tinject(key) 时返回值自动推断为 T | undefined。运行时 InjectionKey 与普通 Symbol 无差异。


题目 3:以下哪种方式可以让子组件只读访问父组件的状态?

A. provide('state', reactive({ count: 0 })) B. provide('state', ref(0)) C. provide('state', readonly(reactive({ count: 0 }))) D. provide('state', computed(() => state.count))

答案:C

解析readonly() 返回一个 Proxy,拦截 set 操作并发出警告,实现只读访问。A、B 选项子组件可直接修改状态,D 选项的 computed 默认只读但只能返回单个值,无法传递整个状态对象。


题目 4:在 SSR 中,app.provide() 的主要风险是什么?

A. 内存泄漏。 B. 单例污染,不同请求共享状态。 C. 类型推断失败。 D. 性能下降。

答案:B

解析app.provide() 在应用实例上注入,若同一应用实例服务多个请求(如未正确创建请求级应用),不同请求会共享注入的值,导致用户 A 的数据被用户 B 看到。解决方法是每个请求创建独立的 app 实例。


题目 5:Vue 3 的 provide/inject 与 React Context 的关键性能差异是什么?

A. Vue 的查找速度更快。 B. Vue 的重渲染粒度更细(属性级)。 C. Vue 支持更多消费者。 D. Vue 的内存占用更低。

答案:B

解析:Vue 3 的响应式系统基于属性级依赖追踪,inject 的组件仅在其依赖的属性变化时重渲染。React Context 的所有消费者在 value 引用变化时全部重渲染,需要通过 useMemo 或拆分 Context 优化。


10.2 填空题

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

答案provides,原型链(prototype chain)

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


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

答案Symbol

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


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

答案O(d)O(d)

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


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

答案refreactive

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


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

答案:请求级

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


10.3 编程题

题目 1:实现一个类型安全的 useCounter Composable,使用 provide/inject 在组件树中共享计数器状态,支持 incrementdecrementreset 操作,并保证子组件只读访问 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.4 思考题

题目 1:在什么场景下,应当优先选择 provide/inject 而非 Pinia?请列举至少三个场景并说明理由。

参考答案

  1. 组件库内部协作:如 FormFormItemInput 三层组件的校验状态传递。组件库不应强制用户安装 Pinia,provide/inject 是 Vue 原生 API,零依赖。

  2. 主题与国际化:主题与国际化是应用级配置,与组件树绑定。使用 provide/inject 可在应用根组件一次性提供,所有子组件共享,无需 Pinia store 的复杂性。

  3. 插件向应用注入服务:Vue 插件(如 HTTP 客户端、日志服务)通过 app.provide() 注入,使用 inject 消费。这是 Vue 插件的官方模式,Pinia 不适合此场景。

  4. 请求级 SSR 状态:Nuxt 等框架的请求级状态(如当前用户)通过 provide/inject 实现,每个请求独立应用实例,Pinia 需要额外配置。

  5. 临时局部状态共享:某个复杂表单内部的多个子组件共享状态,但不应全局化。使用 provide/inject 限定作用域,避免污染全局。


题目 2:分析 Vue 3 provide/inject 的原型链查找机制,为何不采用 Map 结构?两者的性能差异如何?

参考答案

原型链查找的优势

  1. 内存效率:子组件的 provides 通过 Object.create(parent.provides) 创建,仅存储自身新增的键,未修改时共享父组件的 provides 引用,节省内存。
  2. 延迟创建:仅当子组件调用 provide 时才创建独立 provides,未调用的组件零开销。
  3. 自动继承:原型链天然支持链式查找,无需手动维护 Map 树。

Map 结构的劣势

  1. 内存占用:每个组件需维护完整 Map,即使无 provide 也需空 Map。
  2. 查找复杂:需要手动递归查找父组件的 Map,复杂度相同但实现繁琐。
  3. 更新困难:父组件 provide 新值时,需手动通知所有子组件的 Map。

性能差异

  • 原型链查找:O(d)O(d)dd 为组件深度,每次属性访问触发原型链遍历。
  • Map 查找:O(d)O(d),每次需递归调用 parent.provides.get(key)

实际性能差异极小(原型链查找是 JS 引擎高度优化的操作),但原型链实现的内存效率与代码简洁性更优。


题目 3:在大型企业应用中,如何设计一套基于 provide/inject 的依赖注入框架,支持服务生命周期管理(单例、请求、组件级)?

参考答案

设计思路

  1. 服务注册中心:定义 ServiceContainer 接口,管理服务的注册与解析。
interface ServiceContainer {
  register<T>(key: InjectionKey<T>, factory: () => T, scope: 'singleton' | 'request' | 'component'): void;
  resolve<T>(key: InjectionKey<T>): T;
}
  1. 作用域隔离
  • 单例app.provide(key, factory()),应用级单例。
  • 请求:在 SSR 中,每个请求创建独立 appapp.provide(key, factory())
  • 组件级provide(key, factory()),组件子树单例。
  1. 装饰器风格
@Injectable({ scope: 'singleton' })
class UserService {
  // ...
}
  1. Composable 封装
export function useService<T>(key: InjectionKey<T>): T {
  const service = inject(key);
  if (!service) {
    throw new Error(`Service ${String(key)} not provided`);
  }
  return service;
}
  1. 生命周期钩子
interface ServiceLifecycle {
  onInit?: () => void;
  onDestroy?: () => void;
}

实现要点

  • 使用 effectScope 管理响应式副作用。
  • 使用 provide/inject 实现作用域继承。
  • 使用 InjectionKey<T> 保证类型安全。
  • 支持 HMR(热重载)时清理旧实例。

与 Angular DI 的对比

  • Vue 方案更轻量,无独立 DI 容器,依赖组件树。
  • 类型安全通过 InjectionKey 实现,无装饰器复杂度。
  • 作用域控制通过组件层级自然实现。

未来扩展

  • 支持服务依赖图(自动注入依赖)。
  • 支持异步服务初始化(结合 Suspense)。
  • 支持服务替换(测试时 mock)。

11. 参考文献 | References

本文档参考了以下学术文献、官方文档与技术专著,遵循 ACM Reference Format。

11.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

11.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

11.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

11.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.

11.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.6 在线资源

[22] Vue School. 2024. Vue 3 Composition API. Retrieved July 20, 2026 from https://vueschool.io/courses/vue-3-composition-api

[23] Vue Mastery. 2024. Vue 3 Reactivity. Retrieved July 20, 2026 from https://www.vuemastery.com/courses/vue-3-reactivity/vue3-reactivity/

[24] Stack Overflow. 2024. Vue.js 3 provide/inject Questions. Retrieved July 20, 2026 from https://stackoverflow.com/questions/tagged/vue.js+provide+inject


12. 延伸阅读 | Further Reading

12.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 的最佳实践。

12.2 论文与 RFC

  1. Vue 3 Reactivity RFChttps://github.com/vuejs/rfcs

    • Vue 官方的 RFC 列表,包含响应式系统、provide/inject 的设计讨论。
  2. Vue 3 Source Codehttps://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 的本质。

12.3 在线资源

  1. Vue Schoolhttps://vueschool.io/

    • Vue 官方推荐的在线学习平台,包含 provide/inject 的视频教程。
  2. Vue Masteryhttps://www.vuemastery.com/

    • Vue 进阶学习平台,深入讲解 Composition API 与响应式系统。
  3. VueUsehttps://vueuse.org/

    • Vue Composition API 工具库,包含 createGlobalStateuseProvideLocal 等实用工具。
  4. Vue Devtoolshttps://devtools.vuejs.org/

    • Vue 官方调试工具,支持 provide/inject 的可视化。
  5. Component Partyhttps://component-party.dev/

    • 多框架组件 API 对比,包含 provide/inject 在 Vue、React、Svelte 中的对比。

12.4 开源项目参考

  1. Element Plushttps://github.com/element-plus/element-plus

    • Vue 3 组件库,FormForm-Item 等组件大量使用 provide/inject
  2. Vuetify 3https://github.com/vuetifyjs/vuetify

    • Material Design 组件库,主题系统基于 provide/inject
  3. Nuxt 3https://github.com/nuxt/nuxt

    • Vue 3 元框架,useNuxtApp()useRuntimeConfig() 等基于 provide/inject
  4. Vue Router 4https://github.com/vuejs/router

    • 官方路由库,通过 provide/inject 共享路由信息。
  5. Piniahttps://github.com/vuejs/pinia

    • 官方状态管理库,内部使用 provide/inject 实现插件机制。

12.5 社区与讨论

  1. Vue Discordhttps://discord.com/invite/vue

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

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

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

    • Vue 社区,分享 provide/inject 的使用经验。
  5. Stack Overflowhttps://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>stringnumber
  • value:注入值,可以是任意类型(refreactive、普通对象、函数等)。

返回值:无。

调用时机:必须在 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 函数中调用。

解决:确保 injectsetup() 同步执行期间调用,使用 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/injectprops/emits、Pinia 等通信方式,避免过度使用或误用。

返回入门指南