一、先看一个场景:层层传值的噩梦
假设组件嵌套结构是这样的:
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,或者你想听什么也可以点播!