08|provide / inject 依赖注入
第二章:组件模型与通信纵览 / 08。当很多中间组件只是在转交同一份稳定上下文时,可以让祖先提供、后代注入;这不是隐藏任意全局状态的捷径。
什么时候值得跨层注入
主题、表单上下文、标签页控制器这类能力由某个祖先创建,深层后代都属于它的作用域,中间组件通常不需要理解该值。若只是父子两层,props/emits 更透明;若任意组件都要共享且生命周期与组件树无关,应考虑状态管理而不是不断扩大注入范围。
最小可运行主题上下文
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
}<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><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><script setup lang="ts">
import ThemeProvider from './ThemeProvider.vue'
import ThemeToggle from './ThemeToggle.vue'
</script>
<template>
<ThemeProvider>
<div><ThemeToggle /></div>
</ThemeProvider>
</template>值是怎样被找到和更新的
ThemeProvider在自己的setup阶段用themeKey登记上下文。ThemeToggle调用inject(themeKey)时,Vue 沿组件父链寻找最近的同 key 提供者。- 注入的 ref 不会在脚本中自动解包;模板会像普通 ref 一样自动解包显示。
- 后代调用
setTheme,真正的写操作仍发生在 provider 创建的闭包中。 readonly(theme)阻止消费者直接改值,让修改入口保持集中。
InjectionKey<T> 同时把 Symbol 与值类型绑定起来,提供方和消费方若写错结构会一起得到类型错误。字符串 key 在大型应用和插件之间更容易冲突。
最近提供者形成嵌套作用域
若一个 ThemeProvider 内再放另一个同 key 的 provider,内层后代只会拿到内层值;外层兄弟仍拿外层值。这适合局部覆盖主题、表单和国际化配置,也意味着“注入到错误值”时应先查看组件祖先链,而不是只搜 key 的定义。
用普通对象推演最近提供者
下面只模拟查找,不调用Vue,也不替代组件例子。保存为 .mjs 可直接运行:
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,后代才能随其变化更新;提供普通字符串只会得到当时的字符串值:
provide(themeKey, { theme: theme.value, setTheme }) // 丢失 ref 身份同样,不要对注入 ref 做一次性解构取值后期待它继续更新。保留 ref,或在模板中使用它。
缺失依赖要明确选择
必须存在的结构性依赖,像正文 useTheme 一样尽早抛出清晰错误。真正可选的依赖可以给默认值:
const density = inject('density', 'comfortable')默认工厂创建昂贵对象时,第三个参数要标明它是工厂:
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-CNprovider,再放一个初始为en-US的内层 provider;两边按钮分别只修改最近 provider 的值。- 不把可写 ref 直接交给消费者,不使用字符串 key。
参考答案:查看第 2 章第 08 题参考答案。
通关自检:你能从消费组件向上找到最近 provider,并说明响应式值由谁创建、谁可以修改、缺少 provider 时发生什么。