在组件树中,如果祖先组件想给任意深度的后代组件传递数据,一层层通过 props 逐级转发会非常繁琐且容易出错。Vue 提供了 provide 和 inject 这对 API,让祖先组件可以直接向所有子孙组件“注入”数据,中间层的组件无需关心或转发。
基础用法:传递静态数据
在祖先组件中使用 provide 提供数据,在后代组件中使用 inject 获取数据,就像在一个作用域里声明变量并在需要的地方使用它一样。
选项式 API:
// 祖先组件
export default {
provide() {
return {
appName: 'My App',
version: '1.0.0'
}
}
}
// 任意后代组件
export default {
inject: ['appName', 'version'],
mounted() {
console.log(this.appName) // 'My App'
}
}
组合式 API(推荐,更灵活):
<script setup>
import { provide } from 'vue'
// 祖先组件
provide('appName', 'My App')
provide('version', '1.0.0')
</script>
<script setup>
import { inject } from 'vue'
// 后代组件
const appName = inject('appName')
const version = inject('version', '0.0.0') // 可提供默认值
</script>
核心痛点:如何让注入的数据保持响应式?
provide 传递的如果是普通的字符串或数字,它只是一个静态快照。即便祖先组件后续修改了这个值,后代组件接收到的依然是旧值——注入的过程只发生一次。
要让后代组件自动跟随数据变化,需要提供一个响应式数据或一个能计算出响应式结果的函数。
正确的响应式注入方式(组合式 API):
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
// 直接提供 ref,后代可通过 .value 访问和修改,但破坏了单向数据流
provide('count', count) // 不推荐直接提供可写 ref
// 更好的做法:提供只读值,保护数据
provide('count', readonly(count)) // 后代只能读取,不能修改
// 或者提供更新方法,保持单向数据流
function increment() {
count.value++
}
provide('count', readonly(count))
provide('increment', increment)
</script>
<script setup>
import { inject } from 'vue'
const count = inject('count') // 这是一个 ref,具有响应式
const increment = inject('increment')
</script>
<template>
<!-- 自动响应变化 -->
<p>计数:{{ count }}</p>
<button @click="increment">+1</button>
</template>
需要注意的是,如果直接 provide('count', count),后代组件拿到的是一个可写的 ref,可以直接 count.value = 10 在任意位置修改祖先的状态。这在大型应用中会使得数据流难以追踪,通常建议通过 readonly 包裹,或直接提供更新方法(如 increment),保持数据流的清晰和可预测性。
选项式 API 的响应式处理:
在选项式 API 中,provide 可以是一个函数返回对象,但要让数据响应式,需要使用 Vue 2 的 Vue.observable() 或 Vue 3 中直接返回 ref/reactive 包裹的数据(在 setup 中使用 provide / inject 配合 getCurrentInstance,但通常直接迁移到组合式 API 更自然)。若仍坚持纯选项式,可利用 computed 或通过函数形式返回响应式数据:
// 祖先组件 (Vue 3 选项式 + setup)
export default {
setup() {
const count = Vue.ref(0)
Vue.provide('count', Vue.readonly(count))
Vue.provide('increment', () => { count.value++ })
return { count }
}
}
后代用 inject 拿到的就是 ref,会自动解包吗?在选项式组件中注入的 ref 不会自动解包(因为 this 上不直接是 .value),需要手动在 computed 中转一次,或者直接使用组合式 API 的 <script setup>(强烈建议)。
使用 Symbol 作为注入名,避免命名冲突
在大型项目里,建议使用 Symbol 作为注入的 key,因为 Symbol 是唯一值,不同祖先组件用相同字符串 key 发生冲突的概率为零。
// keys.js
export const COUNT_KEY = Symbol('count')
export const INCREMENT_KEY = Symbol('increment')
// 祖先
import { COUNT_KEY, INCREMENT_KEY } from './keys'
provide(COUNT_KEY, readonly(count))
provide(INCREMENT_KEY, increment)
// 后代
const count = inject(COUNT_KEY)
const increment = inject(INCREMENT_KEY)
实战场景与注意事项
- 哪些数据应该用 provide/inject?
适合跨多层组件共享的全局性配置(主题、语言、权限信息、表单上下文),或者某个组件域内的公共状态(如 Tabs 组件的激活状态、Form 的校验规则)。
- 不要滥用。对于跨页面级的全局状态(如用户登录信息、购物车),Pinia 是更好的选择,因为它拥有 DevTools 调试、持久化、时间旅行等能力。
provide/inject更适合模块内部或组件库内部的上下文传递。 - 注入默认值与可选性:
inject的第二个参数是默认值,如果不提供且祖先也未提供,无法获取时会返回undefined。如果想要强制要求必须提供,可以显式抛出一个 Error 或使用inject的第三个参数(一个工厂函数,用于抛出错误)。 - 响应式依赖追踪:如果提供的是
reactive对象,后代组件只会追踪实际访问的属性变化,这是正常的;但如果提供的是一个普通对象的引用,并替换整个对象,注入的是第一次提供时的引用,不会响应。因此正确的做法是提供响应式容器(ref/reactive)本身,而不是它的.value。
// 错误:提供的是一个普通值,后代不响应
const config = reactive({ theme: 'light' })
provide('config', config) // 正确:提供 reactive 对象,后代响应内部属性变化
config = reactive({ theme: 'dark' }) // 重新赋值并不会影响 provide 注入的东西,因为引用变了
// 应该使用 ref 包裹:
const config = ref({ theme: 'light' })
provide('config', readonly(config)) // 后代访问 config.value.theme,响应式
总结:provide/inject 是解决“祖先 → 任意后代”通信的优雅方案,但必须注意响应式数据的提供方式,确保后代能够感知数据变化。在组合式 API 下,直接提供 ref/reactive 并用 readonly 保护是最佳实践。