Vue3 依赖注入:provide/inject 详解

小飞兽 Vue.js 369 次阅读 2026-06-02

Vue3 依赖注入:provide/inject 详解

概述

provide / inject 是 Vue3 提供的一组跨组件层级传递数据的 API,解决了 Prop Drilling(逐层透传 props)问题。当组件嵌套较深时,无需在每一层都声明 props,直接从祖先组件向深层子孙组件注入数据,是 Vue3 组件通信体系中的重要一环。

为什么需要 provide/inject

假设有 A → B → C → D 四层组件,A 有用户信息需要传给 D。如果用 props,需在 B、C 都声明 props: ['user']。如果用 Pinia 显得过重。此时 provide / inject 是最轻量的方案:A provide,B/C 无需感知,D 直接 inject。

基础语法

// 祖先组件中提供
import { provide, ref, reactive, readonly } from 'vue'

// 基本用法:provide(key, value)
provide('appName', '我的应用')
provide('version', 3)

// 推荐:传入 ref/reactive,保持响应式
const userInfo = ref({ name: '林小鸣', role: 'admin' })
provide('userInfo', userInfo)                    // 保持响应式
provide('userInfoReadonly', readonly(userInfo))  // 只读副本,防修改

// 深度子孙组件注入
import { inject } from 'vue'

const userInfo = inject('userInfo')
const appName = inject('appName', '默认值')     // 带默认值

// 注入 reactive 对象
const state = inject('appState')                 // 祖先 provide 的 reactive 对象

完整代码示例

// ========== 祖先组件:App.vue ==========
<script setup>
import { ref, reactive, provide, readonly, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 基本类型注入
provide('appName', '山山问答')
provide('version', '3.0.0')
provide('buildTime', '2026-01-15')

// 响应式用户状态(ref)
const currentUser = ref({
  id: 1001,
  name: '林小鸣',
  avatar: 'https://example.com/avatar/1001.png',
  permissions: ['read', 'write', 'admin']
})

// 响应式应用状态(reactive)
const appState = reactive({
  theme: 'dark',
  language: 'zh-CN',
  sidebarOpen: true,
  notifications: []
})

// 计算属性(只读,防止注入方修改)
const userPermissions = computed(() => currentUser.value.permissions)

// 提供方法(允许注入方调用祖先方法修改状态)
function updateTheme(newTheme) {
  appState.theme = newTheme
}

function addNotification(msg) {
  appState.notifications.push({
    id: Date.now(),
    message: msg,
    timestamp: new Date().toISOString()
  })
}

function hasPermission(permission) {
  return currentUser.value.permissions.includes(permission)
}

// 只读共享(保护性注入)
provide('currentUser', currentUser)                          // 可写(响应式)
provide('currentUserReadonly', readonly(currentUser))        // 只读
provide('appState', appState)
provide('userPermissions', userPermissions)
provide('updateTheme', updateTheme)
provide('addNotification', addNotification)
provide('hasPermission', hasPermission)

// 提供默认值示例(注入方未提供时使用)
provide('fallbackSetting', '默认配置值')
</script>

<template>
  <div class="app" :class="appState.theme">
    <h1>{{ appName }} v{{ version }}</h1>
    <ChildComponent />
    <p style="margin-top: 1rem; font-size: 0.8rem;">
      当前用户: {{ currentUser.name }} | 权限: {{ currentUser.permissions.join(', ') }}
    </p>
  </div>
</template>

<style scoped>
.app { padding: 1rem; transition: all 0.3s; }
.app.dark { background: #1a1a1a; color: #e0e0e0; }
.app.light { background: #ffffff; color: #333; }
</style>

// ========== 中间组件:BreadcrumbNav.vue(无需知道注入的具体内容)==========
<script setup>
// 这个组件只是路由导航,无需感知用户信息或应用状态
// 也不需要向下传递任何 props
</script>

<template>
  <nav class="breadcrumb">
    <slot />
  </nav>
</template>

// ========== 深层注入组件:UserProfile.vue ==========
<script setup>
import { inject, computed, ref, onMounted } from 'vue'

// —— 注入响应式数据 ——
const currentUser = inject('currentUser')              // ref<User>
const appState = inject('appState')                    // reactive<AppState>
const hasPermission = inject('hasPermission')          // 函数

// —— 注入只读数据(保护性使用)——
const userReadonly = inject('currentUserReadonly')

// —— 注入带默认值的数据 ——
// 如果祖先未 provide 'fallbackSetting',则使用 '默认配置值'
const fallback = inject('fallbackSetting', '默认配置值')

// —— 注入函数调用 ——
// 不能直接修改 currentUser.name,但可以通过祖先提供的方法修改
function switchTheme() {
  const newTheme = appState.theme === 'dark' ? 'light' : 'dark'
  inject('updateTheme')(newTheme)
}

function testPermission() {
  const canAdmin = hasPermission('admin')
  console.log('是否有 admin 权限:', canAdmin)
}

// —— 派生值(依赖注入的响应式数据)——
const displayName = computed(() => {
  return `${currentUser.value.name}(ID: ${currentUser.value.id})`
})

const notificationCount = computed(() => appState.notifications.length)

// —— 模拟添加通知 ——-
function pushNotification() {
  inject('addNotification')(`用户 ${currentUser.value.name} 触发了一条通知`)
}

onMounted(() => {
  console.log('UserProfile mounted,当前用户:', currentUser.value.name)
})
</script>

<template>
  <div class="user-profile">
    <h3>{{ displayName }}</h3>

    <div class="info-row">
      <span>头像:</span>
      <img :src="currentUser.avatar" alt="avatar" width="40" height="40" />
    </div>

    <div class="info-row">
      <span>当前主题:</span>
      <button @click="switchTheme">
        切换到 {{ appState.theme === 'dark' ? '浅色' : '深色' }}模式
      </button>
    </div>

    <div class="info-row">
      <span>通知 ({{ notificationCount }}):</span>
      <button @click="pushNotification">添加通知</button>
    </div>

    <div class="permissions">
      <h4>权限测试:</h4>
      <button @click="testPermission">检查 admin 权限</button>
    </div>

    <p style="margin-top: 0.5rem; font-size: 0.8rem; color: #888;">
      只读数据示例: {{ userReadonly.name }}(无法修改)| fallback: {{ fallback }}
    </p>
  </div>
</template>

<style scoped>
.user-profile { border: 1px solid #ccc; padding: 1rem; border-radius: 8px; margin-top: 0.5rem; }
.info-row { display: flex; align-items: center; gap: 0.5rem; margin: 0.5rem 0; }
.permissions { margin-top: 0.5rem; }
</style>

运行效果说明

页面顶部显示应用名称、版本和当前用户信息。深层 UserProfile 组件无需从父组件接收任何 props,直接 inject 获取用户数据和应用状态。点击"切换主题"按钮,通过注入的 updateTheme 函数修改祖先组件的状态,所有使用 appState.theme 的地方(包括 App 组件本身)自动同步响应。点击"添加通知"按钮,appState.notifications 数组增加,通知数量实时更新。

常见问题

Q1:provide / inject 是响应式的吗?修改注入的值会更新视图吗?

是的,provide 传递 refreactive 时,子孙组件 inject 获取的是同一个响应式引用,修改会双向同步。但建议在注入时使用 readonly() 包裹需要保护的数据,防止深层组件直接修改状态导致数据流混乱。修改应通过祖先组件提供的方法进行,这也是单一数据源原则的体现。

Q2:如果注入的数据 key 不存在,会报错吗?如何做容错?

inject() 如果找不到对应 key 且未提供默认值,会抛出警告并返回 undefined。推荐使用带默认值的形式:const value = inject('key', 'defaultValue')。对于对象类型默认值,传入 null 或空对象 {} 即可。注意:如果祖先提供了 null 而非未提供,则默认值不会被使用。

Q3:provide / inject 和 Pinia / Vuex 如何选择?

provide/inject 是组件级别的依赖注入,适合在组件树内共享状态(如主题、语言设置、当前用户信息)。Pinia/Vuex 是全局状态管理,适合跨多个不相关组件共享数据,或状态需要持久化、SSR 支持等场景。简单场景用 provide/inject 即可,无需引入额外依赖;中大型项目推荐 Pinia 统一管理全局状态。

延伸阅读

  • <a href="#">Vue3 组合式 API 完全指南</a> —— 组合式 API 核心概念与实战
  • <a href="#">Vue3 响应式系统深度解析:ref vs reactive</a> —— 响应式数据的底层原理
  • <a href="#">Vue3 Teleport 组件深度解析</a> —— 任意位置渲染组件内容