Vue.js: provide / inject
最后更新:2026-08-26
provide 和 inject 是 Vue 3 的跨层级通信 API——祖先组件 provide 数据,任何后代组件(不管多深)都可以 inject 使用。它解决了"props 透传"(prop drilling)的痛点。
Vue 3 的 provide 支持响应式数据(ref / reactive),后代组件 inject 后会自动响应。本课帮你掌握这套机制和 5 大实战场景。
1. 你将学到
provide/inject基本语法- Symbol 类型作为 inject key
- 响应式 provide(ref / reactive)
- 修改 provide 的 2 种方式
- 与 props / event bus / Pinia 的对比
- 5 大实战场景(主题 / 用户 / i18n)
- 与 provide / inject 相关的 3 个反模式
2. 一个"主题色"传 5 层的 prop drilling 噩梦
(1) 痛点:App → Layout → Header → UserMenu → Button,5 层透传
Alice 的后台仪表盘需要支持主题切换:
VUE
<!-- ❌ 翻车版:5 层 prop drilling -->
<!-- App.vue -->
<Layout :theme="theme" :user="user" :locale="locale">
<router-view />
</Layout>
<!-- Layout.vue -->
<Header :theme="theme" :user="user" :locale="locale" />
<!-- Header.vue -->
<UserMenu :theme="theme" :user="user" :locale="locale" />
<!-- UserMenu.vue -->
<ThemeButton :theme="theme" />
<LocaleSelector :locale="locale" />
产品经理 Charlie 又追加了 3 个配置项:
"Alice,还要加侧边栏折叠配置、通知设置和功能开关。8 个 props 传 5 层组件,这根本没法维护。"
(2) Vue provide / inject 解法:祖先注入,后代接收
VUE
<!-- App.vue 祖先 -->
<script setup>
const { ref, provide } = Vue
const theme = ref('light')
const user = ref({ name: 'Alice' })
const locale = ref('zh-CN')
// ✅ 一次 provide,所有后代可 inject
provide('theme', theme)
provide('user', user)
provide('locale', locale)
</script>
<!-- UserMenu.vue 任意深层后代 -->
<script setup>
const { inject } = Vue
// ✅ 直接获取,无需 props 透传
const theme = inject('theme')
const user = inject('user')
const locale = inject('locale')
</script>
<template>
<button :class="theme">Toggle {{ user.name }}</button>
<p>{{ locale }}</p>
</template>
5 层嵌套 → 1 行 provide + 1 行 inject。
(3) 收益
使用 provide / inject 后:
- 组件嵌套深度:不限(任意后代都能 inject)
- props 数量:5+ → 0(不再透传)
- 新增配置:1 个 provide 即可
- 可维护性:父组件集中管理全局状态
3. provide / inject 基本语法
(1) provide 注入数据
VUE
<!-- App.vue 祖先组件 -->
<script setup>
const { ref, provide } = Vue
const theme = ref('light')
const user = { name: 'Alice' }
// 1. 注入静态值
provide('appName', 'My Admin')
// 2. 注入响应式数据(ref)
provide('theme', theme)
// 3. 注入响应式数据(reactive)
provide('user', user)
// 4. 注入方法
provide('updateUser', (newUser) => {
user.value = newUser
})
</script>
(2) inject 接收数据
VUE
<!-- Child.vue 任意后代组件 -->
<script setup>
const { inject } = Vue
// 1. 基础接收
const theme = inject('theme')
// 2. 带默认值(祖先没 provide 时使用)
const theme = inject('theme', 'light')
// 3. 带工厂函数默认值
const theme = inject('theme', () => 'light')
// 4. 类型断言(TypeScript)
const theme = inject<string>('theme', 'light')
</script>
(3) 完整示例
VUE
<!-- Ancestor.vue -->
<script setup>
const { ref, provide } = Vue
const count = ref(0)
const user = reactive({ name: 'Alice', age: 25 })
provide('count', count) // ref
provide('user', user) // reactive
provide('config', { theme: 'dark' }) // 对象
provide('reset', () => { count.value = 0 }) // 方法
</script>
<!-- DeepChild.vue(任意深层) -->
<script setup>
const { inject } = Vue
const count = inject('count') // 响应式
const user = inject('user') // 响应式
const config = inject('config') // 普通对象
const reset = inject('reset') // 函数
// 修改
function increment() {
count.value++ // ✅ 祖先组件也会响应
}
</script>
4. Symbol 类型作为 inject key
(1) 为什么要用 Symbol?
字符串 key 容易冲突(多个 provide 用同名 key 会覆盖)。Symbol 唯一,避免冲突。
VUE
<!-- keys.js - 集中管理所有 provide key -->
<script>
export const THEME_KEY = Symbol('theme')
export const USER_KEY = Symbol('user')
export const LOCALE_KEY = Symbol('locale')
export const CONFIG_KEY = Symbol('config')
</script>
VUE
<!-- Ancestor.vue 祖先组件 -->
<script setup>
const { provide, ref } = Vue
import { THEME_KEY, USER_KEY, LOCALE_KEY } from './keys'
provide(THEME_KEY, ref('light'))
provide(USER_KEY, ref({ name: 'Alice' }))
provide(LOCALE_KEY, ref('zh-CN'))
</script>
VUE
<!-- DeepChild.vue 任意后代 -->
<script setup>
const { inject } = Vue
import { THEME_KEY } from './keys'
// ✅ 类型安全:必须用 Symbol 才能 inject
const theme = inject(THEME_KEY)
const user = inject(USER_KEY)
</script>
(2) 5 大优势
| 优势 | 说明 |
|---|---|
| 唯一性 | Symbol 全局唯一,不会冲突 |
| 类型安全 | TypeScript 能精确推断 |
| 集中管理 | 所有 key 集中在一个文件 |
| 重构友好 | 修改 key 不影响其他 |
| 可读性 | 看 key 名就知道用途 |
5. 响应式 provide 详解
(1) 3 种 provide 模式
VUE
<!-- Ancestor.vue -->
<script setup>
const { ref, reactive, provide } = Vue
// 1. 注入 ref(响应式)
const count = ref(0)
provide('count', count)
// 2. 注入 reactive(响应式)
const user = reactive({ name: 'Alice' })
provide('user', user)
// 3. 注入只读对象(非响应式)
const config = { theme: 'dark' }
provide('config', config)
</script>
(2) 2 种修改方式
VUE
<!-- 方式 1:祖先直接修改(推荐) -->
<!-- Ancestor.vue -->
<script setup>
const { ref, provide } = Vue
const theme = ref('light')
provide('theme', theme)
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
<!-- 方式 2:后代通过 inject 引用修改 -->
<!-- DeepChild.vue -->
<script setup>
const { inject } = Vue
const theme = inject('theme')
function toggleTheme() {
theme.value = 'dark' // ✅ 改的是 ref,祖先和其他后代都响应
}
</script>
(3) 5 大响应式场景
| 场景 | 用法 |
|---|---|
| 主题切换 | ref + provide |
| 用户登录 | reactive + provide |
| i18n locale | ref + provide + 切换方法 |
| 全局 loading | ref(false) + provide |
| 全局消息提示 | reactive + provide |
6. 5 大实战场景
(1) 场景 1:主题切换
JS
// keys.js
export const THEME_KEY = Symbol('theme')
VUE
<!-- App.vue 祖先 -->
<script setup>
const { ref, provide } = Vue
import { THEME_KEY } from './keys'
const theme = ref('light')
provide(THEME_KEY, theme)
</script>
<!-- ThemeButton.vue 后代 -->
<script setup>
const { inject } = Vue
import { THEME_KEY } from './keys'
const theme = inject(THEME_KEY)
</script>
<template>
<button @click="theme.value = theme === 'light' ? 'dark' : 'light'">
{{ theme === 'light' ? '🌞' : '🌙' }}
</button>
</template>
(2) 场景 2:用户认证
JS
// keys.js
export const AUTH_KEY = Symbol('auth')
VUE
<!-- App.vue -->
<script setup>
const { reactive, provide } = Vue
import { AUTH_KEY } from './keys'
const auth = reactive({
user: null,
isLoggedIn: false,
login(credentials) { /* API call */ },
logout() { this.user = null; this.isLoggedIn = false }
})
provide(AUTH_KEY, auth)
</script>
<!-- UserMenu.vue 后代 -->
<script setup>
const { inject } = Vue
import { AUTH_KEY } from './keys'
const auth = inject(AUTH_KEY)
</script>
<template>
<div v-if="auth.isLoggedIn">{{ auth.user.name }}</div>
<button v-else @click="auth.login(creds)">Login</button>
</template>
(3) 场景 3:i18n
JS
// keys.js
export const I18N_KEY = Symbol('i18n')
VUE
<!-- App.vue -->
<script setup>
const { ref, provide, computed } = Vue
import { I18N_KEY } from './keys'
const messages = {
'zh-CN': { hello: '你好', welcome: '欢迎' },
'en-US': { hello: 'Hello', welcome: 'Welcome' }
}
const locale = ref('zh-CN')
const t = computed(() => (key) => messages[locale.value][key])
provide(I18N_KEY, { locale, t })
</script>
<!-- Hello.vue 后代 -->
<script setup>
const { inject } = Vue
import { I18N_KEY } from './keys'
const { t } = inject(I18N_KEY)
</script>
<template>
<h1>{{ t('hello') }}, {{ t('welcome') }}</h1>
</template>
(4) 场景 4:全局 Loading
VUE
<!-- App.vue -->
<script setup>
const { ref, provide } = Vue
const loading = ref(false)
provide('loading', loading)
</script>
<!-- AnyComponent.vue 后代 -->
<script setup>
const { inject } = Vue
const loading = inject('loading')
async function fetchData() {
loading.value = true
await fetch('/api/data')
loading.value = false
}
</script>
<template>
<button @click="fetchData">Refresh</button>
</template>
(5) 场景 5:主题 + 业务数据
VUE
<!-- App.vue -->
<script setup>
const { ref, reactive, provide } = Vue
const appState = reactive({
theme: 'light',
user: { name: 'Alice' },
permissions: ['read', 'write'],
config: { sidebar: true }
})
provide('appState', appState)
</script>
7. provide / inject vs 其他方案
(1) 5 种通信方式对比
| 维度 | props/emit | provide/inject | event bus | Pinia | mitt |
|---|---|---|---|---|---|
| 父子通信 | ⭐⭐⭐⭐⭐ | - | - | - | - |
| 跨层级 | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 兄弟通信 | ❌(要父中转) | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 响应式 | ✅ | ✅ | ❌(要手动) | ✅ | ❌ |
| 类型安全 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| DevTools | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 学习曲线 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
(2) 5 大选择场景
| 场景 | 推荐方案 |
|---|---|
| 父子通信 | props/emit |
| 跨层级全局状态(主题/认证/i18n) | provide/inject |
| 中大型应用全局状态 | Pinia(Phase 4.2 学) |
| 简单事件总线 | mitt / tiny-emitter |
| 复杂业务状态管理 | Pinia + Vue Router |
8. 完整示例:电商后台主题切换
▶ 示例:集中管理 keys + 一次性 provide
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
.app { padding: 1rem; }
.app.light { background: #fff; color: #333; }
.app.dark { background: #1f2937; color: #f9fafb; }
.btn { padding: 6px 12px; margin: 4px; border: 1px solid #ddd; background: #42b883; color: white; border-radius: 4px; cursor: pointer; }
</style>
<div id="app">
<app-root></app-root>
</div>
<script>
const { createApp, ref, reactive, provide } = Vue
// 集中管理的 Symbol keys
const THEME_KEY = Symbol('theme')
const USER_KEY = Symbol('user')
const LOCALE_KEY = Symbol('locale')
const CART_KEY = Symbol('cart')
// App.vue:一次性 provide 4 个全局状态
const AppRoot = {
setup() {
// 主题
const theme = ref('light')
function setTheme(newTheme) { theme.value = newTheme }
// 用户
const user = reactive({ id: 1, name: 'Alice', role: 'admin' })
// 国际化
const locale = ref('zh-CN')
const messages = {
'zh-CN': { home: '首页', cart: '购物车' },
'en-US': { home: 'Home', cart: 'Cart' }
}
const t = (key) => messages[locale.value][key]
// 购物车
const cart = reactive({ items: [{ name: 'iPhone', price: 999 }], total: 999 })
provide(THEME_KEY, { theme, setTheme })
provide(USER_KEY, { user })
provide(LOCALE_KEY, { locale, t })
provide(CART_KEY, { cart })
return {}
},
template: `
<deep-child></deep-child>
`
}
// 任意深层后代组件(多层嵌套)
const DeepChild = {
components: { GrandChild: { template: '<grand-grand-child></grand-grand-child>' },
GrandGrandChild: { components: { Consumer }, template: '<consumer></consumer>' } },
template: `
<div style="border: 1px dashed #42b883; padding: 1rem;">
<p>DeepChild 中间层(不消费 provide)</p>
<grand-child></grand-child>
</div>
`
}
// 最终消费者:inject 4 个 key
const Consumer = {
setup() {
const { theme, setTheme } = inject(THEME_KEY)
const { user } = inject(USER_KEY)
const { t } = inject(LOCALE_KEY)
const { cart } = inject(CART_KEY)
return { theme, setTheme, user, t, cart }
},
template: `
<div :class="['app', theme]">
<p>用户: {{ user.name }} ({{ user.role }})</p>
<p>i18n: {{ t('home') }} | {{ t('cart') }}</p>
<p>购物车: {{ cart.items.length }} 件, ${{ cart.total }}</p>
<button class="btn" @click="setTheme(theme === 'light' ? 'dark' : 'light')">
切换主题(当前: {{ theme }})
</button>
</div>
`
}
const app = createApp(AppRoot)
// 注册后代组件
app.component('DeepChild', {
components: {
GrandChild: {
components: { Consumer },
template: `
<div style="border: 1px dashed #42b883; padding: 1rem; margin-top: 0.5rem;">
<p>GrandChild 中间层</p>
<consumer></consumer>
</div>
`
}
},
template: `
<div style="border: 1px dashed #42b883; padding: 1rem;">
<p>DeepChild 中间层(不消费 provide)</p>
<grand-child></grand-child>
</div>
`
})
app.mount('#app')
</script>
▶ 示例:Symbol 注入键(避免冲突)
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<tester></tester>
</div>
<script>
const { createApp, ref, provide, inject } = Vue
// 不同 Symbol 即使描述相同,也是不同 key
const THEME_KEY = Symbol('theme')
const ANOTHER_THEME_KEY = Symbol('theme') // ✅ 不同 key,不会冲突
const Tester = {
setup() {
provide(THEME_KEY, ref('light'))
provide(ANOTHER_THEME_KEY, ref('dark'))
const theme1 = inject(THEME_KEY)
const theme2 = inject(ANOTHER_THEME_KEY)
return { theme1, theme2 }
},
template: `
<div>
<p>THEME_KEY: {{ theme1 }}</p>
<p>ANOTHER_THEME_KEY: {{ theme2 }}</p>
<p style="color: #999;">两个 Symbol 描述相同但互不冲突</p>
</div>
`
}
createApp(Tester).mount('#app')
</script>
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 字符串 key 冲突 | 覆盖 | 用 Symbol |
| 非祖先 provide | undefined | 检查组件层级 |
| 修改 readonly provide | 警告 | 祖先改,后代只读 |
| 忘记默认值 | undefined | inject(key, defaultValue) |
| 大量 provide | 难维护 | 集中到 keys.js |
▶ 示例:5 大性能对比
| 模式 | 性能 | 适用 |
|---|---|---|
provide('key', value) |
⭐⭐⭐⭐⭐ | 静态值 |
provide('key', ref) |
⭐⭐⭐⭐⭐ | 响应式值 |
provide('key', reactive) |
⭐⭐⭐⭐ | 复杂对象 |
provide('key', computed) |
⭐⭐⭐⭐⭐ | 派生值 |
provide('key', function) |
⭐⭐⭐⭐ | 操作方法 |
❓ 常见问题
Q provide 和 props 选哪个?
A 父子直接通信用 props(明确、类型安全)。跨层级(3+ 层)用 provide,避免 prop drilling。大型应用全局状态用 Pinia(Phase 4.2)。
Q provide 的数据是响应式的吗?
A 取决于提供的数据。
provide('key', ref) 响应式,provide('key', {a: 1}) 不响应(普通对象)。推荐 provide ref / reactive。Q provide 必须在 setup 顶层吗?
A 是的。
provide 必须在 <script setup> 顶层调用(生命周期钩子内不允许),否则 provide 不生效。Q inject 后能修改 provide 的值吗?
A 能改 ref.value,但建议只在祖先组件修改(通过 provide 方法)。直接改 ref 会让数据流不清晰。
Q Symbol key 在哪里定义?
A 单独一个文件
src/keys.js,export 所有 Symbol。所有用到的组件都从这个文件 import。Q provide / inject 和 Pinia 怎么选?
A 小项目(< 5 个全局状态)用 provide/inject 够用。中大型项目(10+ 全局状态)用 Pinia,DevTools 支持更好。
Q provide 数据祖先组件卸载后会怎样?
A 所有后代组件也一起卸载(Vue 组件树生命周期)。inject 会重新向上找祖先。
📖 小节
- provide / inject 是 Vue 3 跨层级通信 API
- provide 注入(祖先),inject 接收(任意后代)
- 5 种 provide 类型:静态值 / ref / reactive / computed / function
- Symbol key 避免冲突,集中管理在 keys.js
- 5 大实战场景:主题 / 认证 / i18n / loading / 业务数据
- 与 props 对比:父子用 props,跨层级用 provide
- 与 Pinia 对比:小项目 provide,大项目 Pinia
📝 作业
-
基础题(难度⭐) 实现一个简单的主题切换:
- keys.js 定义 THEME_KEY
- App.vue provide theme (ref) + setTheme 方法
- ThemeButton.vue inject theme + setTheme
- 点击按钮切换 light/dark
-
进阶题(难度⭐⭐) 实现用户认证 provide/inject:
- AUTH_KEY 注入 user + login + logout 方法
- App.vue 初始化 user = null
- LoginPage.vue 调用 login(creds) 模拟登录
- Header.vue 根据 isLoggedIn 显示不同 UI
-
挑战题(难度⭐⭐⭐) 实现完整的电商后台全局状态系统:
- 4 个 keys:THEME / USER / CART / LOCALE
- App.vue 一次 provide 4 个全局状态
- 5 个不同层级组件使用(Header / Sidebar / ProductList / Cart / Footer)
- 跨组件通信:Header 触发登出 → Sidebar 自动更新
- 主题切换 + i18n 切换 + 购物车增减
- 用 TypeScript 强类型
- 5 种 provide 模式(值/ref/reactive/computed/function)