Vue 3 中 provide/inject 结合响应式数据

FreeGuideOnline 最新 2026-07-07

vue


**后代组件(注入数据):**

```vue
<script setup>
import { inject } from 'vue'

const message = inject('message') // 'hello'
</script>

但是,如果是静态字符串 hello,注入后并不会保持响应性。为了让数据变化能自动传递到所有后代,需要将响应式数据通过 provide 传递。

3. 传递响应式数据:ref 与 reactive

Vue 3 的响应式核心是 refreactive。只要在 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)