在 Vue 3 的组件通信体系中,provide 与 inject 是一对容易被低估却极为强大的 API。它们最初在 Vue 2 中作为 provide/inject 选项出现,Vue 3 将其升级为组合式函数,并赋予了响应式能力、Symbol key、默认值处理等新特性。对于深层嵌套组件通信、插件开发、全局状态轻量管理等场景,这对 API 往往比 props 逐层传递或引入 Pinia 更加优雅。然而,灵活性的背后也隐藏着若干陷阱。本文将从基础回顾出发,深入探讨 provide/inject 的高级用法与工程实践中的注意事项。
一、基础回顾:从选项式到组合式
在 Vue 3 的 <script setup> 中,provide 和 inject 以函数形式使用:
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 第二个参数为默认值
</script>
关键点在于:provide 提供的如果是 ref 或 reactive 对象,注入方拿到的就是响应式对象本身。这意味着后代组件可以直接修改祖先的状态,也可以基于它派生 computed。这与 Vue 2 中 provide 默认非响应式的行为有本质区别。
二、高级用法一:用 Symbol 作为注入 key
字符串 key 存在命名冲突风险,尤其在大型项目或组件库中。推荐使用 Symbol 作为 key,并集中导出:
// injectionKeys.js
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')
<!-- 提供方 -->
<script setup>
import { provide, ref } from 'vue'
import { ThemeKey } from './injectionKeys'
provide(ThemeKey, ref('dark'))
</script>
<!-- 注入方 -->
<script setup>
import { inject } from 'vue'
import { ThemeKey } from './injectionKeys'
const theme = inject(ThemeKey)
</script>
Symbol 的唯一性从根本上杜绝了意外覆盖,同时配合 TypeScript 还能获得完整的类型推导。
三、高级用法二:封装类型安全的注入函数
直接在组件中调用 inject 会导致类型断言散落各处。更好的做法是为每个 key 封装一个自定义 hook:
// useTheme.ts
import { inject, type Ref } from 'vue'
import { ThemeKey } from './injectionKeys'
export function useTheme(): Ref<string> {
const theme = inject(ThemeKey)
if (!theme) {
throw new Error('useTheme() 必须在提供了 ThemeKey 的组件树中使用')
}
return theme
}
这种模式有两个好处:一是类型安全,调用方无需关心 Ref 类型;二是快速失败,当注入缺失时立刻抛出明确错误,而不是让 undefined 在后续逻辑中引发难以定位的 bug。
四、高级用法三:提供只读状态与修改方法
直接暴露可写的 ref 会破坏“单向数据流”的直觉。更推荐的做法是提供只读状态和显式的修改函数:
<script setup>
import { provide, ref, readonly } from 'vue'
import { ThemeKey } from './injectionKeys'
const theme = ref('dark')
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
provide(ThemeKey, {
theme: readonly(theme),
toggleTheme
})
</script>
后代组件只能读取 theme,修改必须通过 toggleTheme。这样既保留了响应式追踪,又让状态变更路径清晰可审计。
五、高级用法四:在插件中全局提供依赖
app.provide() 允许在应用级别注入依赖,非常适合插件开发:
// plugins/i18n.js
export default {
install(app, options) {
const locale = ref(options.defaultLocale)
app.provide('i18n', {
locale,
t: (key) => options.messages[locale.value][key]
})
}
}
任何组件都可以通过 inject('i18n') 获取国际化能力,无需层层传递 props。
六、注意事项:响应式丢失的常见陷阱
陷阱一:解构导致响应式丢失。 如果 provide 的是一个 reactive 对象,注入方解构后会失去响应性:
// ❌ 错误:theme 不再是响应式
const { theme } = inject(ThemeKey)
// ✅ 正确:保留整个对象,或使用 toRefs
const state = inject(ThemeKey)
// 或
const { theme } = toRefs(inject(ThemeKey))
陷阱二:provide 的值本身不是响应式的。 如果提供的是普通字符串或数字,后续祖先组件修改它,后代不会更新。必须用 ref 或 reactive 包装。
陷阱三:在异步组件或 setup 之外调用 inject。 inject 必须在 setup 同步执行阶段调用,否则无法正确关联组件实例。常见错误是在 onMounted 或 setTimeout 中调用 inject。
陷阱四:默认值的求值时机。 inject 的第二个参数如果是函数,会被当作工厂函数调用;如果只是普通值,则直接返回。若默认值依赖其他注入,应使用工厂函数形式:
const config = inject(ConfigKey, () => ({ debug: false }))
七、何时不该用 provide/inject
provide/inject 并非万能。以下场景应谨慎:
- 跨路由或跨应用的全局状态:应使用 Pinia 或全局 store,因为
provide/inject的作用域受组件树限制。 - 需要严格追踪状态变更来源的复杂业务:显式 props/emit 或状态管理库更利于调试。
- 仅一两层嵌套的通信:props 更直观,引入注入反而增加理解成本。
结语
provide 与 inject 是 Vue 3 组合式 API 中一把精巧的“手术刀”,适合处理深层依赖传递、插件能力注入和轻量级状态共享。掌握 Symbol key、类型安全封装、只读暴露与响应式陷阱,能让你在工程中既享受其便利,又避免隐蔽的 bug。记住一条原则:注入的是能力,而不是随意共享的可变状态。当依赖关系清晰、契约明确时,provide/inject 会成为组件架构中优雅的一环。
未经允许不得转载:任鹏个人博客 » Vue 3 中的依赖注入:provide 与 inject 高级用法与注意事项


朋友圈点赞图在线生成源码