Vue3 依赖注入:provide/inject 详解
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 传递 ref 或 reactive 时,子孙组件 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> —— 任意位置渲染组件内容