一、先看一个场景:层层传值的噩梦

假设组件嵌套结构是这样的:

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,或者你想听什么也可以点播!

更多推荐