provide/inject 基本用法
Provide/Inject 与跨层通信 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
当组件层级有四层,而最底层需要最顶层的数据时,逐层传 props 会变成「prop 钻孔」。provide/inject 是 Vue 给出的跨层通信方案。这一节掌握它的基本用法、响应式传递规则,以及用 Symbol 做依赖 key 的工程习惯。
问题场景
一个主题系统:App.vue 持有主题状态,Layout → Sidebar → Menu → MenuItem 四层组件都要读主题。逐层传 props 意味着四个中间组件都要声明一个自己根本不用的 theme prop——改一次结构,四个文件跟着动。这正是 provide/inject 的典型场景:祖先提供、任意深度的后代注入,中间层无感。
核心解释
祖先组件调用 provide(key, value),后代组件调用 inject(key, defaultValue):
<!-- App.vue(祖先) -->
<script setup>
import { provide, ref } from "vue";
const theme = ref("light");
function toggleTheme() {
theme.value = theme.value === "light" ? "dark" : "light";
}
provide("theme", { theme, toggleTheme });
</script>
<template>
<Layout />
</template>
后代组件只写三行:const { theme, toggleTheme } = inject("theme"),模板里就能直接读 theme、调 toggleTheme()——中间层完全不需要声明任何 prop。
两个关键点:
- 响应式靠 ref 本身传递。
provide("theme", theme)提供的是 ref 对象,后代注入后拿到的也是同一个 ref,所以theme.value变化会自动更新。如果 provide 的是theme.value(普通字符串),变化就丢了。 - provide 的值可以随时改,但只有「对象属性」或「ref 的 value」的变化能被追踪;直接替换 provide 的引用不会通知已注入的后代。
用 Symbol 或 injectionKey 作为 key:字符串 key 在大型项目里容易撞名,而且没有类型提示。Vue 提供 InjectionKey<T> 类型(TypeScript):
// keys.ts
import type { InjectionKey, Ref } from "vue";
export interface ThemeContext {
theme: Ref<string>;
toggleTheme: () => void;
}
export const themeKey: InjectionKey<ThemeContext> = Symbol("theme");
祖先用 provide(themeKey, { theme: ref("light"), toggleTheme }) 提供,后代用 const ctx = inject(themeKey) 注入,并在为空时抛出「themeKey 未提供」的明确错误——契约在类型上就写死了。
这样 key 是模块单例,不会撞名;inject 的返回类型也精确。Vue 官方推荐在应用级依赖(比如插件)里用这种模式。
inject 的默认值:inject("theme", { theme: ref("light"), toggleTheme: () => {} }) 让后代在祖先没提供时也能工作——适合可独立发布的组件库。
示例拆解
一个完整的主题切换链路,从 App 到最深层:
<!-- App.vue -->
<script setup>
import { computed, provide, ref } from "vue";
import Layout from "./components/Layout.vue";
const theme = ref("light");
const toggleTheme = () => {
theme.value = theme.value === "light" ? "dark" : "light";
};
const isDark = computed(() => theme.value === "dark");
provide("theme", { theme, isDark, toggleTheme });
</script>
<template>
<div :class="{ dark: isDark }">
<Layout />
</div>
</template>
<!-- components/MenuItem.vue -->
<script setup>
import { inject } from "vue";
const { isDark, toggleTheme } = inject("theme");
</script>
<template>
<li>
<button @click="toggleTheme">
{{ isDark ? "切到浅色" : "切到深色" }}
</button>
</li>
</template>
验证:点击按钮,App 的 class 变化、所有注入 isDark 的组件同步更新。打开 Vue Devtools 的组件树,可以看到 provide/inject 的依赖关系(Devtools 会标注 injected 来源)。
动手任务
- 把上面的字符串 key 改造成
InjectionKey(可先用 JSDoc 注释模拟),确认注入处有类型提示。 - 给
inject加默认值版本,让MenuItem在独立使用时(没有祖先提供主题)显示浅色并不报错。 - 破坏响应式:把
provide("theme", theme.value)改成普通字符串,观察后代不再更新,再改回来。
常见错误
- provide 了非响应式值:
provide("theme", theme.value)传的是快照,后代永远看到初始值。传 ref 或 reactive 对象。 - 在
onMounted之后 provide:provide 必须在 setup 同步阶段调用,异步后提供不会生效。 - key 用字符串且项目大了以后撞名:两个模块都 provide("config"),后者覆盖前者。用 Symbol/InjectionKey。
- 把 provide/inject 当全局状态库:它没有集中管理、没有 devtools 时间旅行、没有持久化。应用级状态用 Pinia(V11)。
检查与复盘
- 能写出「祖先 provide ref、后代 inject 并读写」的完整链路
- 能解释为什么必须传 ref 而不是 ref.value
- 能说出 InjectionKey 解决的两个问题
参考资料
- Vue 官方文档「依赖注入」:provide/inject 的响应式与 key 设计
- Vue 官方文档「组合式 API 常见问答」:与状态管理的边界
读完这一课了?
标记完成后,工作台会安排接下来的学习。