Vue.js: provide / inject

最后更新:2026-08-26

provideinject 是 Vue 3 的跨层级通信 API——祖先组件 provide 数据,任何后代组件(不管多深)都可以 inject 使用。它解决了"props 透传"(prop drilling)的痛点。

Vue 3 的 provide 支持响应式数据(ref / reactive),后代组件 inject 后会自动响应。本课帮你掌握这套机制和 5 大实战场景。

1. 你将学到


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 后:


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>
逻辑代码 89 行(超过 40 行限制,仅展示)

▶ 示例: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 会重新向上找祖先。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的主题切换:

    • keys.js 定义 THEME_KEY
    • App.vue provide theme (ref) + setTheme 方法
    • ThemeButton.vue inject theme + setTheme
    • 点击按钮切换 light/dark
  2. 进阶题(难度⭐⭐) 实现用户认证 provide/inject:

    • AUTH_KEY 注入 user + login + logout 方法
    • App.vue 初始化 user = null
    • LoginPage.vue 调用 login(creds) 模拟登录
    • Header.vue 根据 isLoggedIn 显示不同 UI
  3. 挑战题(难度⭐⭐⭐) 实现完整的电商后台全局状态系统:

    1. 4 个 keys:THEME / USER / CART / LOCALE
    2. App.vue 一次 provide 4 个全局状态
    3. 5 个不同层级组件使用(Header / Sidebar / ProductList / Cart / Footer)
    4. 跨组件通信:Header 触发登出 → Sidebar 自动更新
    5. 主题切换 + i18n 切换 + 购物车增减
    6. 用 TypeScript 强类型
    7. 5 种 provide 模式(值/ref/reactive/computed/function)
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏