Skip to content

08|provide / inject 依赖注入 ​

第二章:组件模型与通信纵览 / 08。当很多中间组件只是在转交同一份稳定上下文时,可以让祖先提供、后代注入;这不是隐藏任意全局状态的捷径。

什么时候值得跨层注入 ​

主题、表单上下文、标签页控制器这类能力由某个祖先创建,深层后代都属于它的作用域,中间组件通常不需要理解该值。若只是父子两层,props/emits 更透明;若任意组件都要共享且生命周期与组件树无关,应考虑状态管理而不是不断扩大注入范围。

最小可运行主题上下文 ​

ts
import { inject, type InjectionKey, type Ref } from 'vue'

export type ThemeName = 'light' | 'dark'

export interface ThemeContext {
  theme: Readonly<Ref<ThemeName>>
  setTheme(next: ThemeName): void
}

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

export function useTheme() {
  const context = inject(themeKey)
  if (!context) {
    throw new Error('useTheme() 必须在 ThemeProvider 后代中调用')
  }
  return context
}
vue
<script setup lang="ts">
import { provide, readonly, ref } from 'vue'
import { themeKey, type ThemeName } from './theme-context'

const theme = ref<ThemeName>('light')

function setTheme(next: ThemeName) {
  theme.value = next
}

provide(themeKey, {
  theme: readonly(theme),
  setTheme
})
</script>

<template>
  <section :data-theme="theme">
    <slot />
  </section>
</template>
vue
<script setup lang="ts">
import { useTheme } from './theme-context'

const { theme, setTheme } = useTheme()
</script>

<template>
  <button
    type="button"
    @click="setTheme(theme === 'light' ? 'dark' : 'light')"
  >
    当前 {{ theme }},点击切换
  </button>
</template>
vue
<script setup lang="ts">
import ThemeProvider from './ThemeProvider.vue'
import ThemeToggle from './ThemeToggle.vue'
</script>

<template>
  <ThemeProvider>
    <div><ThemeToggle /></div>
  </ThemeProvider>
</template>

值是怎样被找到和更新的 ​

  1. ThemeProvider 在自己的 setup 阶段用 themeKey 登记上下文。
  2. ThemeToggle 调用 inject(themeKey) 时,Vue 沿组件父链寻找最近的同 key 提供者。
  3. 注入的 ref 不会在脚本中自动解包;模板会像普通 ref 一样自动解包显示。
  4. 后代调用 setTheme,真正的写操作仍发生在 provider 创建的闭包中。
  5. readonly(theme) 阻止消费者直接改值,让修改入口保持集中。

InjectionKey<T> 同时把 Symbol 与值类型绑定起来,提供方和消费方若写错结构会一起得到类型错误。字符串 key 在大型应用和插件之间更容易冲突。

最近提供者形成嵌套作用域 ​

若一个 ThemeProvider 内再放另一个同 key 的 provider,内层后代只会拿到内层值;外层兄弟仍拿外层值。这适合局部覆盖主题、表单和国际化配置,也意味着“注入到错误值”时应先查看组件祖先链,而不是只搜 key 的定义。

用普通对象推演最近提供者 ​

下面只模拟查找,不调用Vue,也不替代组件例子。保存为 .mjs 可直接运行:

js
import assert from 'node:assert/strict'
const key = Symbol('locale')
const outer = { [key]: 'zh-CN' }
const inner = Object.create(outer)
assert.equal(inner[key], 'zh-CN')
inner[key] = 'en-US'
assert.equal(inner[key], 'en-US')
assert.equal(outer[key], 'zh-CN')
const sibling = Object.create(outer)
assert.equal(sibling[key], 'zh-CN')

查找先命中自身,没有才沿父链继续;内层覆盖没有改写外层。Vue的provide/inject可以用这条查找路径理解,但原型链本身不会让字符串变响应式。组件树决定寻找哪个上下文,ref/proxy决定值改变后谁会更新,它们是两层机制。

响应性不会凭空产生 ​

提供 ref 或 computed,后代才能随其变化更新;提供普通字符串只会得到当时的字符串值:

ts
provide(themeKey, { theme: theme.value, setTheme }) // 丢失 ref 身份

同样,不要对注入 ref 做一次性解构取值后期待它继续更新。保留 ref,或在模板中使用它。

缺失依赖要明确选择 ​

必须存在的结构性依赖,像正文 useTheme 一样尽早抛出清晰错误。真正可选的依赖可以给默认值:

ts
const density = inject('density', 'comfortable')

默认工厂创建昂贵对象时,第三个参数要标明它是工厂:

ts
const service = inject(serviceKey, () => createService(), true)

不要给必需依赖一个悄悄工作的假默认值,否则组件离开 provider 后会以不同语义继续运行,测试也可能掩盖装配错误。

提供时机与应用级提供 ​

组件内的 provide() 和 inject() 应在 setup 同步阶段调用,确保 Vue 能关联当前实例。应用启动时也可用 app.provide 提供插件级依赖,但它的作用域覆盖整个应用,更需要稳定 key 和清晰替换策略。

练习:可嵌套的语言环境 ​

实现 locale-context.ts、LocaleProvider.vue 与 LocaleButton.vue:

  • 类型只允许 'zh-CN' | 'en-US'。
  • context 暴露只读 locale 与 setLocale,使用 InjectionKey 和 Symbol。
  • LocaleButton 显示当前值并在两种语言间切换;离开 provider 时抛出可读错误。
  • App.vue 放一个外层 zh-CN provider,再放一个初始为 en-US 的内层 provider;两边按钮分别只修改最近 provider 的值。
  • 不把可写 ref 直接交给消费者,不使用字符串 key。

参考答案:查看第 2 章第 08 题参考答案。

通关自检:你能从消费组件向上找到最近 provider,并说明响应式值由谁创建、谁可以修改、缺少 provider 时发生什么。

用心记录每一步成长