Vue 3 中 provide/inject 结合响应式数据
vue
**后代组件(注入数据):**
```vue
<script setup>
import { inject } from 'vue'
const message = inject('message') // 'hello'
</script>
但是,如果是静态字符串 hello,注入后并不会保持响应性。为了让数据变化能自动传递到所有后代,需要将响应式数据通过 provide 传递。
3. 传递响应式数据:ref 与 reactive
Vue 3 的响应式核心是 ref 和 reactive。只要在 provide 时传入响应式对象本身,后代组件注入后仍保持响应性。
3.1 使用 ref 提供基础类型
<!-- 祖先组件 -->
<script setup>
import { ref, provide } from 'vue'
const count = ref(0)
provide('count', count)
function increment() {
count.value++
}
</script>
<!-- 任意后代组件 -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
</script>
<template>
<p>当前计数:{{ count }}</p>
</template>
关键点:provide 传递的是 count 这个 ref 对象本身,而不是 count.value。注入后,模板中直接使用 count 即可(模板自动解包 ref),或者在 <script> 中通过 .value 访问。
3.2 使用 reactive 提供对象
<!-- 祖先组件 -->
<script setup>
import { reactive, provide } from 'vue'
const user = reactive({
name: '张三',
age: 20
})
provide('user', user)
function updateName(name) {
user.name = name
}
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>
<template>
<p>姓名:{{ user.name }},年龄:{{ user.age }}</p>
</template>
reactive 对象注入后同样是响应式的,所有后代组件都能实时同步状态变化。
4. 提供修改方法:保证单一数据流向
直接在任意后代组件中修改注入的响应式数据是可行的(因为引用同一个对象),但这会导致数据变化来源难以追踪,违背单向数据流的原则。最佳实践是:由提供者统一提供修改数据的方法,后代只通过方法触发变更。
<!-- 祖先组件 -->
<script setup>
import { ref, provide } from 'vue'
const todos = ref(['阅读', '运动'])
// 提供数据的同时,提供修改方法
provide('todos', {
todos,
addTodo: (text) => {
todos.value.push(text)
},
removeTodo: (index) => {
todos.value.splice(index, 1)
}
})
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const { todos, addTodo, removeTodo } = inject('todos')
</script>
<template>
<ul>
<li v-for="(todo, index) in todos" :key="index">
{{ todo }}
<button @click="removeTodo(index)">删除</button>
</li>
</ul>
<button @click="addTodo('新任务')">添加</button>
</template>
这样做的好处:
- 所有修改逻辑集中在提供者中,便于维护和调试。
- 即使后代组件较多,也不会出现“谁改了这个值”的困惑。
5. 使用 readonly 防止意外修改
如果某个数据只允许后代组件读取,不允许直接修改,可以使用 readonly 包装一下再 provide。
<script setup>
import { ref, provide, readonly } from 'vue'
const config = ref({
theme: 'light',
lang: 'zh-CN'
})
provide('config', readonly(config))
</script>
后代组件注入的 config 将不可被修改(修改会触发警告),只有提供者可以变更 config.value,从而保证数据流向的可控性。
当结合修改方法一起提供时,可如下封装:
provide('config', {
config: readonly(config),
setTheme: (theme) => {
config.value.theme = theme
}
})
这样即安全又保留了必要的修改能力。
6. 大型应用中的考量:provide / inject vs Pinia
Vue 3 官方推荐的状态管理方案是 Pinia。那么什么时候应该用 provide/inject + 响应式数据,什么时候用 Pinia?
| 场景 | 推荐方案 |
|---|---|
| 组件树内部局部的共享状态(例如表单区块、选项卡组件) | provide + inject |
| 跨多个页面、跨整个应用的全局状态 | Pinia |
| 需要 Devtools 支持、时间旅行调试 | Pinia |
| 极简的场景,不想引入额外依赖 | provide + inject |
provide/inject 组合响应式数据可以看作轻量级的状态管理,特别适合组件封装、逻辑内聚的场景。但要注意:默认情况下 provide 的数据不具备单例性,每个祖先实例会创建自己的一份数据,这恰好符合局部共享的预期。
7. 常见踩坑与注意事项
-
注入时提供默认值
如果祖先组件可能没有provide,可以使用inject的第二个参数设置默认值,避免undefined错误:const count = inject('count', 0) -
不要解构 reactive
如果provide的是reactive对象,后代注入后不要解构,否则会丢失响应性。如需部分取值,可使用toRefs或直接访问属性。 -
provide 的 key 建议使用 Symbol
大型应用中使用 Symbol 作为 key 可以避免命名冲突:// shared/keys.js export const countKey = Symbol('count') // 祖先组件 import { countKey } from './keys' provide(countKey, count) // 后代组件 import { countKey } from './keys' const count = inject(countKey)