Vue依赖注入:provide/inject 让你告别层层传值,案例多到眼花
一、先看一个场景:层层传值的噩梦
假设组件嵌套结构是这样的:
text
App(曾祖父,有用户名数据)
└── Parent(父组件,压根不用用户名)
└── Child(子组件,也不用用户名)
└── GrandChild(曾孙组件,要显示用户名)
如果只用 props,你得这么干:
-
App 把用户名传给 Parent(Parent 根本不关心)
-
Parent 再传给 Child(Child 也不关心)
-
Child 再传给 GrandChild(终于用上了)
这叫 props 逐层透传,中间组件被迫接收跟自己无关的数据。项目一大,这种“无关数据流”会像管道里的杂物一样越积越多。
provide 和 inject 就是来清这个杂物的。
用一张图理解:
text
App(provide 数据)────────────────────────────┐
└── Parent(不接收) │
└── Child(不接收) │
└── GrandChild(inject 直接拿到)←┘
祖先提供数据,后代直接注入使用,中间组件完全不用管。
二、第一个案例:最简单的 provide/inject
祖先把数据“广播”出去,后代“收听”并拿到。
2.1 祖先组件 App.vue
vue
<template>
<div>
<h2>祖先组件</h2>
<!-- 中间组件啥也不用干,直接嵌套 -->
<ChildLevel />
</div>
</template>
<script setup>
import { provide } from 'vue'
import ChildLevel from './ChildLevel.vue'
// 祖先组件有一份数据:主题名称
const theme = '深色模式'
// provide(键名, 值) 把数据提供给所有后代组件
// 第一个参数 'appTheme' 是一个字符串 key,随便起,但要和 inject 时一致
// 第二个参数是要共享的数据,这里是一个普通字符串
provide('appTheme', theme)
// 以后任何后代组件,不管套了多少层,都能用 inject('appTheme') 拿到
</script>
2.2 中间组件 ChildLevel.vue
vue
<template>
<div>
<h3>子组件(中间层)</h3>
<p>这个组件根本不需要 theme 数据,但子组件需要</p>
<!-- 继续嵌套下一层 -->
<GrandChild />
</div>
</template>
<script setup>
import GrandChild from './GrandChild.vue'
// 注意:中间层没有引入 provide 也没有引入 inject
// 它完全不需要关心 theme 这个数据
</script>
2.3 后代组件 GrandChild.vue
vue
<template>
<div>
<h4>曾孙组件</h4>
<!-- 直接使用 inject 拿到的数据,就像用本地变量一样 -->
<p>当前主题:{{ theme }}</p>
</div>
</template>
<script setup>
import { inject } from 'vue'
// inject('appTheme') 从祖先组件接收数据
// 参数 'appTheme' 必须和祖先 provide 的第一个参数一致
const theme = inject('appTheme')
</script>
代码拆解:
-
祖先用
provide('钥匙名', 值)把数据“挂”到组件树上。 -
后代用
inject('钥匙名')直接取到值,中间不管隔了多少层都行。 -
如果祖先没有 provide 这个 key,inject 会返回
undefined。你也可以给它一个默认值。
三、案例二:提供响应式数据
上面传的是一个普通字符串,如果数据变了,后代组件能自动更新吗?不能。 要想后代也能实时响应变化,必须提供响应式数据。
3.1 祖先组件
vue
<template>
<div>
<h2>祖先组件</h2>
<p>当前主题:{{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
<ChildLevel />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import ChildLevel from './ChildLevel.vue'
// 用 ref 创建响应式数据
const theme = ref('浅色模式')
// provide 响应式 ref 时,直接传 ref 本身,不要 .value
// 如果写 provide('theme', theme.value),那后代拿到的是静态值,不会更新
provide('appTheme', theme)
function toggleTheme() {
// 切换主题
theme.value = theme.value === '浅色模式' ? '深色模式' : '浅色模式'
}
</script>
3.2 后代组件
vue
<template>
<div>
<h4>曾孙组件</h4>
<p>当前主题:{{ theme }}</p>
<button @click="changeToGreen">换成绿色</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 接收祖先提供的响应式 ref
const theme = inject('appTheme')
function changeToGreen() {
// 后代也可以直接修改它,所有引用此数据的地方都会同步更新
theme.value = '绿色模式'
}
</script>
关键点:
-
provide('appTheme', theme)传的是 ref 对象本身,不是.value。 -
后代
inject拿到的也是同一个 ref 对象,模板里直接用(会自动解包)。 -
后代修改这个 ref 的值,祖先和其他后代都会同步更新。
四、案例三:提供修改数据的方法(更规范的写法)
虽然上面案例中后代可以直接改数据,但这样会造成数据流混乱——谁都能改,出了 bug 很难追溯。最佳实践是:祖先提供数据的同时,也提供修改数据的方法。
4.1 祖先组件
vue
<template>
<div>
<h2>祖先组件</h2>
<p>网站标题:{{ siteTitle }}</p>
<ChildLevel />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import ChildLevel from './ChildLevel.vue'
// 网站标题
const siteTitle = ref('我的网站')
// 提供修改标题的方法,而不是让后代直接改数据
function updateTitle(newTitle) {
// 可以在这里加校验、处理逻辑
if (newTitle.trim()) {
siteTitle.value = newTitle
}
}
// 同时提供数据和修改数据的方法
provide('siteTitle', siteTitle)
provide('updateTitle', updateTitle)
// 后代想要什么就拿什么,数据和操作分离
</script>
4.2 后代组件
vue
<template>
<div>
<h4>曾孙组件</h4>
<p>网站标题:{{ siteTitle }}</p>
<input v-model="newTitle" placeholder="输入新标题" />
<button @click="changeTitle">修改标题</button>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
// 注入数据
const siteTitle = inject('siteTitle')
// 注入修改方法
const updateTitle = inject('updateTitle')
// 本地输入框的值
const newTitle = ref('')
function changeTitle() {
// 调用祖先提供的方法来修改,而不是直接 siteTitle.value = xxx
updateTitle(newTitle.value)
newTitle.value = '' // 清空输入框
}
</script>
好处: 所有修改逻辑都集中在祖先组件里,后代只是“申请修改”,方便管理和调试。
五、案例四:provide 一个“读+写”的计算属性
有时你希望提供一个既有值又能改的变量,但又不想暴露底层的 ref。可以 provide 一个带有 get 和 set 的组合。
vue
<!-- 祖先组件 -->
<script setup>
import { ref, computed, provide } from 'vue'
const count = ref(0)
// 用 computed 的 get/set 包装
const counter = computed({
get: () => count.value,
set: (val) => {
// 可以在 set 里加限制
if (val >= 0 && val <= 100) {
count.value = val
}
}
})
// 把包装好的 computed 提供给后代
provide('counter', counter)
</script>
vue
<!-- 后代组件 -->
<template>
<p>计数:{{ counter }}</p>
<button @click="counter++">加1</button>
<!-- 注意:这里 counter++ 会触发 setter,setter 里可以加限制 -->
</template>
<script setup>
import { inject } from 'vue'
const counter = inject('counter')
</script>
六、案例五:使用 Symbol 作为键名(大型项目推荐)
如果项目很大,多人协作,字符串 key 容易冲突。可以用 Symbol 作为 provide 的 key。
6.1 定义 keys.js
javascript
// keys.js
// Symbol 是 ES6 引入的一种唯一值,每次调用 Symbol() 都生成独一无二的值
// 这样不同模块即使起了同样的描述,也不会冲突
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
6.2 祖先组件
vue
<script setup>
import { ref, provide } from 'vue'
import { THEME_KEY } from './keys.js'
const theme = ref('浅色')
provide(THEME_KEY, theme)
</script>
6.3 后代组件
vue
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './keys.js'
const theme = inject(THEME_KEY)
</script>
好处: 只要 keys.js 文件管理好,就不会有命名冲突,也不怕误覆盖。
七、案例六:全局 provide(在 main.js 中提供)
有些数据整个应用都要用(比如当前登录用户、全局配置),可以在 main.js 中直接 provide。
main.js
javascript
import { createApp } from 'vue'
import App from './App.vue'
import { ref } from 'vue'
const app = createApp(App)
// 在应用级别 provide 数据,所有组件都能拿到
const globalConfig = ref({
apiBaseUrl: 'https://api.example.com',
version: '1.0.0'
})
// app.provide 注册的是应用级别的依赖
app.provide('globalConfig', globalConfig)
app.mount('#app')
任何组件都可以直接 inject('globalConfig') 拿到这个配置,不用在每个页面里重复写。
八、案例七:完整实战——主题切换与通知系统
把上面的知识揉在一起,做一个实际需求:网站有主题切换功能,并且切换主题时所有组件都能收到通知。
8.1 useTheme.js(组合式函数封装)
javascript
// useTheme.js
import { ref, provide, inject } from 'vue'
// Symbol 作为唯一 key
const THEME_KEY = Symbol('theme')
// 提供主题的 hook,只在根组件调用一次
export function provideTheme() {
// 主题
const theme = ref('light')
// 切换次数
const switchCount = ref(0)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
switchCount.value++
}
// 把主题、切换次数、切换方法都提供出去
provide(THEME_KEY, {
theme,
switchCount,
toggleTheme
})
}
// 消费主题的 hook,任何后代组件都能调用
export function useTheme() {
const context = inject(THEME_KEY)
if (!context) {
// 如果没找到,抛一个友好的错误
throw new Error('useTheme 必须在 provideTheme 的后代组件中使用')
}
return context
}
8.2 根组件 App.vue
vue
<template>
<div>
<h1>网站根组件</h1>
<!-- 调用 toggleTheme 来切换主题 -->
<button @click="toggleTheme">切换主题</button>
<p>已切换 {{ switchCount }} 次</p>
<!-- 子页面组件 -->
<HomePage />
<FooterBar />
</div>
</template>
<script setup>
import { provideTheme, useTheme } from './useTheme.js'
import HomePage from './HomePage.vue'
import FooterBar from './FooterBar.vue'
// 在根组件初始化主题
provideTheme()
// 根组件自己也能用
const { theme, switchCount, toggleTheme } = useTheme()
</script>
8.3 HomePage.vue(子页面)
vue
<template>
<div :style="{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }">
<h2>首页</h2>
<p>当前主题:{{ theme }}</p>
</div>
</template>
<script setup>
import { useTheme } from './useTheme.js'
// 直接拿到主题,不用通过 props
const { theme } = useTheme()
</script>
8.4 FooterBar.vue(另一个子组件)
vue
<template>
<footer>
<p>总切换次数:{{ switchCount }}</p>
</footer>
</template>
<script setup>
import { useTheme } from './useTheme.js'
const { switchCount } = useTheme()
</script>
效果:
-
点击按钮,所有组件主题同步切换。
-
切换次数在所有组件中实时显示。
-
所有逻辑封装在
useTheme里,组件里只有业务。
九、provide/inject 和 Props/Emit 该选谁?
| 场景 | 推荐 |
|---|---|
| 父传子(一层) | props |
| 子传父(一层) | emit |
| 祖传孙(三层以上) | provide/inject |
| 全局状态(整个应用) | Pinia(更强大的状态管理) |
| 深层组件需要读某个值但不需要改 | provide/inject |
| 需要复杂的状态管理、中间件 | Pinia |
一句话: 如果只是“让后代知道某个值”,用 provide/inject;如果是跨组件共享复杂状态,用 Pinia。
十、总结
今天我们学会了:
-
provide(key, value):祖先组件提供数据。 -
inject(key):后代组件注入数据。 -
提供响应式数据时传 ref 本身,后代拿到后能实时更新。
-
推荐同时提供修改数据的方法,保持数据流清晰。
-
用 Symbol 作为 key 避免命名冲突。
-
可以在 main.js 中全局 provide 应用级数据。
provide/inject 是 Vue 里解决“隔代传值”的核心武器,配合之前学的组合式函数,能让你的代码结构清晰、复用性高、维护起来不费劲。
有问题评论区说,我挨个回。下篇咱们可以聊聊 Vue 和 TypeScript,或者你想听什么也可以点播!
更多推荐

所有评论(0)