Vue 3 provide inject 跨级组件通信
Vue 3 跨级组件通信:provide 与 inject 完全指南
在 Vue 应用中,当组件层级较深时,如果使用传统的 props 逐层传递数据会变得非常繁琐,而使用全局状态管理(如 Pinia)又显得大材小用。Vue 3 提供的 provide 和 inject 正是解决跨级组件通信的利器,它能让祖先组件向所有子孙组件直接注入数据,无需中间组件接力。
本教程将从基础用法、响应式处理、类型安全到实战技巧,系统讲解这一机制。
为什么需要 provide / inject?
假设组件结构为:App -> Parent -> Child -> GrandChild,现在 App 需要传递一个主题色给 GrandChild。如果只用 props,代码会变成:
<!-- App.vue -->
<Parent :theme="theme" />
<!-- Parent.vue -->
<Child :theme="theme" />
<!-- Child.vue -->
<GrandChild :theme="theme" />
每次引入中间组件,都只是为了“搬运”属性,毫无意义。provide/inject 完美解决了这个问题,让数据可以直接“空降”到目标组件。
基础用法:静态数据注入
1. 在祖先组件中使用 provide
provide 是一个选项,可以是一个对象或返回对象的函数。推荐使用函数形式,以便访问组件实例的 this。
<!-- App.vue -->
<script>
export default {
provide() {
return {
appTheme: 'dark',
appVersion: '1.0.0'
}
}
}
</script>
使用 <script setup> 语法更简洁,需要从 vue 中引入 provide 函数:
<script setup>
import { provide } from 'vue'
provide('appTheme', 'dark')
provide('appVersion', '1.0.0')
</script>
2. 在子孙组件中使用 inject
子孙组件通过 inject 选项或 inject 函数接收数据。
选项式 API:
<!-- GrandChild.vue -->
<script>
export default {
inject: ['appTheme'],
mounted() {
console.log(this.appTheme) // 'dark'
}
}
</script>
组合式 API:
<script setup>
import { inject } from 'vue'
const theme = inject('appTheme')
console.log(theme) // 'dark'
</script>
注意:如果注入的 key 不存在,默认返回 undefined。你可以在 inject 时提供默认值:
const theme = inject('appTheme', 'light') // 第二个参数为默认值
// 或者使用工厂函数
const version = inject('appVersion', () => 'unknown')
注入响应式数据
上述示例提供的是静态字符串,当提供的数据本身需要响应式变化时,直接提供普通值不会让注入方更新。Vue 3 中需要结合 ref 或 reactive 来实现。
推荐做法:提供 ref 或 reactive 对象
祖先组件:
<script setup>
import { ref, provide } from 'vue'
const theme = ref('dark')
provide('appTheme', theme)
// 模拟动态修改
setTimeout(() => {
theme.value = 'light'
}, 3000)
</script>
子孙组件直接使用该 ref:
<script setup>
import { inject } from 'vue'
const theme = inject('appTheme')
// theme 是一个 Ref 对象,可以直接在模板中使用,或通过 .value 访问
watch(theme, (newVal) => {
console.log('主题变化:', newVal)
})
</script>
<template>
<div :class="theme">当前主题:{{ theme }}</div>
</template>
为什么不能直接提供基本类型值?
provide('appTheme', 'dark') 传递的是字符串字面量,一旦注入,它就和提供方的变量失去联系。而传递 ref 对象时,注入方拿到的就是同一个响应式引用,因此数据变化会同步。
提供 reactive 对象 同样可行:
const state = reactive({ theme: 'dark' })
provide('appState', state)
// 子组件
const state = inject('appState')
// 直接使用 state.theme
特殊情况:提供 readonly 保护数据
如果不希望子组件直接修改注入的数据,可以使用 readonly 包装:
import { ref, provide, readonly } from 'vue'
const theme = ref('dark')
provide('appTheme', readonly(theme))
这样,子组件无法通过 theme.value = 'xxx' 修改,只能读取。
使用 Symbol 作为注入键名,避免冲突
当应用中有多处 provide 或第三方库也在使用 provide 时,字符串键名可能发生冲突。推荐使用 Symbol 作为注入键。
创建 injectionKeys.ts 文件统一管理:
// injectionKeys.ts
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
在提供方:
<script setup>
import { provide } from 'vue'
import { THEME_KEY } from './injectionKeys'
provide(THEME_KEY, 'dark')
</script>
在注入方:
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './injectionKeys'
const theme = inject(THEME_KEY)
</script>
这样,即使不同模块的键名相同也不会相互覆盖。
TypeScript 支持:强类型注入
为获得更好的类型提示和错误检查,可结合 TypeScript 使用。
1. 定义注入键的类型
import type { InjectionKey, Ref } from 'vue'
import { ref, provide, inject } from 'vue'
interface ThemeContext {
color: Ref<string>
fontSize: Ref<number>
}
export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')
2. 提供时使用该键
<script setup lang="ts">
const theme: ThemeContext = {
color: ref('blue'),
fontSize: ref(14)
}
provide(themeKey, theme)
</script>
3. 注入时获得完整类型
<script setup lang="ts">
const theme = inject(themeKey)
// theme 的类型为 ThemeContext | undefined,需要类型守卫或提供默认值
if (!theme) {
throw new Error('theme 必须被提供')
}
// 现在 theme.color 是 Ref<string> 类型
</script>
或者使用 inject 的默认值消除 undefined:
const theme = inject(themeKey, { color: ref('black'), fontSize: ref(12) })
实战案例:跨级主题切换
下面是一个完整的主题切换示例,展示 provide 与 inject 如何在真实场景中工作。
目录结构:
src/
components/
ThemeProvider.vue -- 顶级提供者
ThemeConsumer.vue -- 深层消费者
App.vue
ThemeProvider.vue (提供主题及切换方法)
<script setup lang="ts">
import { ref, provide, reactive, readonly } from 'vue'
export interface ThemeState {
color: string
isDark: boolean
}
const theme = reactive<ThemeState>({
color: 'dark',
isDark: true
})
const toggleTheme = () => {
theme.isDark = !theme.isDark
theme.color = theme.isDark ? 'dark' : 'light'
}
// 提供数据和方法,方法用 readonly 包裹防止子组件篡改
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>
<template>
<slot />
</template>
ThemeConsumer.vue (深层子组件消费)
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
if (!theme || !toggleTheme) {
throw new Error('请确保在 ThemeProvider 内使用组件')
}
</script>
<template>
<div :class="theme.color">
<p>当前主题:{{ theme.color }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
App.vue 使用
<template>
<ThemeProvider>
<div class="page">
<h1>应用标题</h1>
<!-- 即使嵌套多层,ThemeConsumer 仍能获取到主题 -->
<ThemeConsumer />
</div>
</ThemeProvider>
</template>
常见问题与注意事项
1. provide / inject 的数据不是响应式的?
请检查是否提供了响应式对象(ref、reactive)。如果提供的是基本类型或普通对象,注入方拿到的只是静态快照。
2. 如何让注入的数据在提供者不可用时拥有默认值?
使用 inject 的第二个参数设置默认值,或是在注入后使用计算属性进行回退。
const theme = inject('theme', { color: 'blue' })
3. 是否可以在 setup 之外的选项式 API 中使用?
可以。provide 和 inject 在选项式 API 中同样可用,只是需要使用 this 访问注入的值。但组合式 API 更加直观和类型友好。
4. 是否会影响性能?
不会。provide/inject 的依赖收集和 props 类似,仅当注入的响应式数据变化时,对应的组件才会重新渲染,与深层嵌套无关。
5. 可以注入方法(函数)吗?
完全可以。这正是实现“依赖注入”的常见方式之一,比如注入 toggleTheme。注意,普通函数无法被 readonly 影响,若想防止子组件替换函数,可单独处理。
总结
| 场景 | 推荐方案 |
|---|---|
| 静态数据传递 | provide('key', 'value') |
| 需要响应式 | provide('key', ref()) |
| 防止子组件修改 | provide('key', readonly(ref)) |
| 复杂类型+类型安全 | 使用 InjectionKey |
| 避免键名冲突 | 使用 Symbol 作为键 |
provide 与 inject 是 Vue 3 组合式 API 中解决跨层级数据共享的核心方案。合理使用它们,既能保持组件树的简洁,又能避免不必要的全局状态管理。不妨在你的下一个项目中实践一下!