引言
在传统的组件跨级传值场景中,父组件要向孙组件传递数据时,必须经历"父组件 → 子组件 → 孙组件 "的层层传递过程。当组件层级很深,而中间组件并不需要这些数据时,这种传递方式会造成很大的代码冗余和性能浪费。今天我们将介绍一种更高效的解决方案------依赖注入 ,通过 provide
和 inject
实现快速的跨级传值。

一、依赖注入的基本使用
1.1 基础用法
在祖先组件中提供数据:
javascript
<!-- APP.vue -->
<script setup>
import { ref, provide } from 'vue'
import User from '@/views/User.vue'
// 提供静态数据
const val = 100
provide('version', val)
</script>
<template>
<div>
<User />
</div>
</template>
在子孙组件中注入数据:
javascript
<!-- User.vue -->
<script setup>
import { ref, inject } from 'vue'
// 注入祖先组件提供的数据
const val = inject('version')
console.log(val) // 输出: 100
</script>
<template>
<div class='box'>
<p>版本号: {{ val }}</p>
</div>
</template>
1.2 实际应用场景
javascript
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
import Layout from '@/components/Layout.vue'
// 提供应用配置
provide('appConfig', {
theme: 'dark',
language: 'zh-CN',
apiBaseUrl: 'https://api.example.com'
})
// 提供用户信息
provide('userInfo', {
name: '张三',
role: '管理员',
permissions: ['read', 'write', 'delete']
})
</script>
<template>
<Layout />
</template>
javascript
<!-- UserProfile.vue (深层嵌套组件) -->
<script setup>
import { inject } from 'vue'
// 直接注入所需数据,无需中间组件传递
const appConfig = inject('appConfig')
const userInfo = inject('userInfo')
</script>
<template>
<div :class="`profile ${appConfig.theme}`">
<h3>{{ userInfo.name }}</h3>
<p>角色: {{ userInfo.role }}</p>
<p>主题: {{ appConfig.theme }}</p>
</div>
</template>
二、传递响应式数据
2.1 响应式数据传递
javascript<script setup>
import { ref, provide } from 'vue'
// 提供响应式数据
const count = ref(100)
const user = ref({
name: '李四',
age: 25
})
provide('count', count)
provide('user', user)
// 提供修改函数,保持单向数据流
const updateCount = (newValue) => {
count.value = newValue
}
provide('updateCount', updateCount)
</script>
2.2 在子组件中使用响应式数据
javascript
<!-- Counter.vue -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
const updateCount = inject('updateCount')
const increment = () => {
updateCount(count + 1)
}
</script>
<template>
<div>
<p>当前计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
2.3 防止数据意外修改
为了保证数据的单向流,可以使用 readonly
包装数据:
javascript
<!-- App.vue -->
<script setup>
import { ref, provide, readonly } from 'vue'
const val = ref(100)
const settings = ref({
theme: 'light',
fontSize: 14
})
// 使用readonly防止子组件直接修改数据
provide('version', readonly(val))
provide('appSettings', readonly(settings))
// 只能通过提供的函数来修改
provide('updateSettings', (newSettings) => {
Object.assign(settings.value, newSettings)
})
</script>
javascript
<!-- Settings.vue -->
<script setup>
import { inject } from 'vue'
const appSettings = inject('appSettings')
const updateSettings = inject('updateSettings')
// 这样会报错,因为appSettings是只读的
// appSettings.theme = 'dark'
// 正确的方式:使用提供的函数
const changeTheme = () => {
updateSettings({ theme: 'dark' })
}
</script>
三、数据注入时的问题处理
3.1 处理注入值不存在的情况
javascript
<script setup>
import { inject } from 'vue'
// 方式1:设置默认值
const version = inject('version111', 10) // 如果'version111'不存在,使用默认值10
// 方式2:提供复杂的默认值
const config = inject('appConfig', {
theme: 'light',
language: 'zh-CN',
apiUrl: 'http://localhost:3000'
})
// 方式3:抛出错误(对于必需依赖)
const requiredDependency = inject('requiredKey')
if (!requiredDependency) {
throw new Error('必需注入的依赖 requiredKey 未提供')
}
// 方式4:使用工厂函数创建默认值
const dynamicValue = inject('dynamicConfig', () => ({
timestamp: Date.now(),
random: Math.random()
}))
</script>
3.2 实际项目中的错误处理
javascript
<!-- SafeInjection.vue -->
<script setup>
import { inject, onMounted } from 'vue'
// 安全的注入函数
function safeInject(key, defaultValue = null) {
const value = inject(key, defaultValue)
if (value === null && defaultValue === null) {
console.warn(`注入的键 "${key}" 不存在且未提供默认值`)
}
return value
}
// 使用安全注入
const userConfig = safeInject('userConfig', {
theme: 'light',
notifications: true
})
const apiService = safeInject('apiService')
if (!apiService) {
console.error('API服务未注入,某些功能可能无法使用')
}
onMounted(() => {
if (!apiService) {
// 降级处理
console.warn('使用本地存储替代API服务')
}
})
</script>
四、名称冲突解决
4.1 名称冲突场景分析
当多个组件提供相同名称的变量时,Vue会按照组件树从上到下 的顺序查找,使用最近祖先提供的值。
javascript
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
import Layout from './Layout.vue'
provide('theme', 'dark') // 提供主题为dark
</script>
<template>
<Layout />
</template>
javascript
<!-- Layout.vue -->
<script setup>
import { provide } from 'vue'
import Content from './Content.vue'
provide('theme', 'light') // 重新提供主题为light
</script>
<template>
<Content />
</template>
javascript
<!-- Content.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
console.log(theme) // 输出: 'light'(使用最近祖先Layout提供的值)
</script>
4.2 解决名称冲突的最佳实践
使用Symbol避免命名冲突:
javascript
// symbols.js
export const AppSymbols = {
THEME: Symbol('theme'),
CONFIG: Symbol('config'),
USER: Symbol('user')
}
export const LayoutSymbols = {
THEME: Symbol('layout-theme'),
SIDEBAR: Symbol('sidebar')
}
javascript
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
import { AppSymbols } from './symbols'
provide(AppSymbols.THEME, 'dark')
provide(AppSymbols.CONFIG, {
apiUrl: 'https://api.example.com'
})
</script>
javascript
<!-- Layout.vue -->
<script setup>
import { provide } from 'vue'
import { LayoutSymbols, AppSymbols } from './symbols'
// 使用不同的Symbol提供布局主题
provide(LayoutSymbols.THEME, 'light')
// 可以继续提供应用级别的配置(不会冲突)
provide(AppSymbols.USER, {
name: '张三',
role: 'admin'
})
</script>
javascript
<!-- Content.vue -->
<script setup>
import { inject } from 'vue'
import { AppSymbols, LayoutSymbols } from './symbols'
// 明确注入特定的值
const appTheme = inject(AppSymbols.THEME) // 'dark'
const layoutTheme = inject(LayoutSymbols.THEME) // 'light'
const user = inject(AppSymbols.USER) // { name: '张三', role: 'admin' }
</script>
五、全局变量的定义与管理
5.1 在main.js中定义全局依赖
在App.vue中放置应用的全局依赖不太合适,更好的做法是在main.js中定义:
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 应用级别的全局依赖
app.provide('appVersion', '1.0.0')
app.provide('apiBaseUrl', 'https://api.example.com')
// 全局服务
app.provide('httpService', {
get: (url) => fetch(url).then(res => res.json()),
post: (url, data) => fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
})
// 全局工具函数
app.provide('utils', {
formatDate: (date) => new Date(date).toLocaleDateString(),
debounce: (func, wait) => {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
})
app.mount('#app')
5.2 在任何组件中使用全局依赖
javascript
<!-- AnyComponent.vue -->
<script setup>
import { inject, ref, onMounted } from 'vue'
// 注入全局依赖
const appVersion = inject('appVersion')
const httpService = inject('httpService')
const utils = inject('utils')
const users = ref([])
const loading = ref(false)
const fetchUsers = utils.debounce(async () => {
loading.value = true
try {
users.value = await httpService.get(`${inject('apiBaseUrl')}/users`)
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}, 300)
onMounted(() => {
fetchUsers()
})
</script>
<template>
<div>
<p>应用版本: {{ appVersion }}</p>
<div v-if="loading">加载中...</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }} - 注册时间: {{ utils.formatDate(user.createdAt) }}
</li>
</ul>
</div>
</template>
5.3 完整的全局状态管理示例
javascript
// globalState.js
import { reactive } from 'vue'
export const globalState = reactive({
user: null,
theme: 'light',
notifications: [],
isLoading: false
})
export const globalActions = {
setUser(user) {
globalState.user = user
},
setTheme(theme) {
globalState.theme = theme
document.documentElement.setAttribute('data-theme', theme)
},
addNotification(message, type = 'info') {
globalState.notifications.push({
id: Date.now(),
message,
type,
timestamp: new Date()
})
},
removeNotification(id) {
globalState.notifications = globalState.notifications.filter(
notification => notification.id !== id
)
}
}
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { globalState, globalActions } from './globalState'
const app = createApp(App)
// 提供全局状态和管理函数
app.provide('globalState', globalState)
app.provide('globalActions', globalActions)
app.mount('#app')
2.2 在子组件中使用响应式数据
javascript
<!-- Counter.vue -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
const updateCount = inject('updateCount')
const increment = () => {
updateCount(count + 1)
}
</script>
<template>
<div>
<p>当前计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
2.3 防止数据意外修改
为了保证数据的单向流,可以使用 readonly
包装数据:
javascript
<!-- App.vue -->
<script setup>
import { ref, provide, readonly } from 'vue'
const val = ref(100)
const settings = ref({
theme: 'light',
fontSize: 14
})
// 使用readonly防止子组件直接修改数据
provide('version', readonly(val))
provide('appSettings', readonly(settings))
// 只能通过提供的函数来修改
provide('updateSettings', (newSettings) => {
Object.assign(settings.value, newSettings)
})
</script>
javascript
<!-- Settings.vue -->
<script setup>
import { inject } from 'vue'
const appSettings = inject('appSettings')
const updateSettings = inject('updateSettings')
// 这样会报错,因为appSettings是只读的
// appSettings.theme = 'dark'
// 正确的方式:使用提供的函数
const changeTheme = () => {
updateSettings({ theme: 'dark' })
}
</script>
三、数据注入时的问题处理
3.1 处理注入值不存在的情况
javascript
<script setup>
import { inject } from 'vue'
// 方式1:设置默认值
const version = inject('version111', 10) // 如果'version111'不存在,使用默认值10
// 方式2:提供复杂的默认值
const config = inject('appConfig', {
theme: 'light',
language: 'zh-CN',
apiUrl: 'http://localhost:3000'
})
// 方式3:抛出错误(对于必需依赖)
const requiredDependency = inject('requiredKey')
if (!requiredDependency) {
throw new Error('必需注入的依赖 requiredKey 未提供')
}
// 方式4:使用工厂函数创建默认值
const dynamicValue = inject('dynamicConfig', () => ({
timestamp: Date.now(),
random: Math.random()
}))
</script>
3.2 实际项目中的错误处理
javascript
<!-- SafeInjection.vue -->
<script setup>
import { inject, onMounted } from 'vue'
// 安全的注入函数
function safeInject(key, defaultValue = null) {
const value = inject(key, defaultValue)
if (value === null && defaultValue === null) {
console.warn(`注入的键 "${key}" 不存在且未提供默认值`)
}
return value
}
// 使用安全注入
const userConfig = safeInject('userConfig', {
theme: 'light',
notifications: true
})
const apiService = safeInject('apiService')
if (!apiService) {
console.error('API服务未注入,某些功能可能无法使用')
}
onMounted(() => {
if (!apiService) {
// 降级处理
console.warn('使用本地存储替代API服务')
}
})
</script>
四、名称冲突解决
4.1 名称冲突场景分析
当多个组件提供相同名称的变量时,Vue会按照组件树从上到下 的顺序查找,使用最近祖先提供的值。
javascript
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
import Layout from './Layout.vue'
provide('theme', 'dark') // 提供主题为dark
</script>
<template>
<Layout />
</template>
javascript
<!-- Layout.vue -->
<script setup>
import { provide } from 'vue'
import Content from './Content.vue'
provide('theme', 'light') // 重新提供主题为light
</script>
<template>
<Content />
</template>
javascript
<!-- Content.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
console.log(theme) // 输出: 'light'(使用最近祖先Layout提供的值)
</script>
4.2 解决名称冲突的最佳实践
使用Symbol避免命名冲突:
javascript
// symbols.js
export const AppSymbols = {
THEME: Symbol('theme'),
CONFIG: Symbol('config'),
USER: Symbol('user')
}
export const LayoutSymbols = {
THEME: Symbol('layout-theme'),
SIDEBAR: Symbol('sidebar')
}
javascript
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
import { AppSymbols } from './symbols'
provide(AppSymbols.THEME, 'dark')
provide(AppSymbols.CONFIG, {
apiUrl: 'https://api.example.com'
})
</script>
javascript
<!-- Layout.vue -->
<script setup>
import { provide } from 'vue'
import { LayoutSymbols, AppSymbols } from './symbols'
// 使用不同的Symbol提供布局主题
provide(LayoutSymbols.THEME, 'light')
// 可以继续提供应用级别的配置(不会冲突)
provide(AppSymbols.USER, {
name: '张三',
role: 'admin'
})
</script>
javascript
<!-- Content.vue -->
<script setup>
import { inject } from 'vue'
import { AppSymbols, LayoutSymbols } from './symbols'
// 明确注入特定的值
const appTheme = inject(AppSymbols.THEME) // 'dark'
const layoutTheme = inject(LayoutSymbols.THEME) // 'light'
const user = inject(AppSymbols.USER) // { name: '张三', role: 'admin' }
</script>
五、全局变量的定义与管理
5.1 在main.js中定义全局依赖
在App.vue中放置应用的全局依赖不太合适,更好的做法是在main.js中定义:
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 应用级别的全局依赖
app.provide('appVersion', '1.0.0')
app.provide('apiBaseUrl', 'https://api.example.com')
// 全局服务
app.provide('httpService', {
get: (url) => fetch(url).then(res => res.json()),
post: (url, data) => fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
})
// 全局工具函数
app.provide('utils', {
formatDate: (date) => new Date(date).toLocaleDateString(),
debounce: (func, wait) => {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
})
app.mount('#app')
5.2 在任何组件中使用全局依赖
javascript
<!-- AnyComponent.vue -->
<script setup>
import { inject, ref, onMounted } from 'vue'
// 注入全局依赖
const appVersion = inject('appVersion')
const httpService = inject('httpService')
const utils = inject('utils')
const users = ref([])
const loading = ref(false)
const fetchUsers = utils.debounce(async () => {
loading.value = true
try {
users.value = await httpService.get(`${inject('apiBaseUrl')}/users`)
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}, 300)
onMounted(() => {
fetchUsers()
})
</script>
<template>
<div>
<p>应用版本: {{ appVersion }}</p>
<div v-if="loading">加载中...</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }} - 注册时间: {{ utils.formatDate(user.createdAt) }}
</li>
</ul>
</div>
</template>
5.3 完整的全局状态管理示例
javascript
// globalState.js
import { reactive } from 'vue'
export const globalState = reactive({
user: null,
theme: 'light',
notifications: [],
isLoading: false
})
export const globalActions = {
setUser(user) {
globalState.user = user
},
setTheme(theme) {
globalState.theme = theme
document.documentElement.setAttribute('data-theme', theme)
},
addNotification(message, type = 'info') {
globalState.notifications.push({
id: Date.now(),
message,
type,
timestamp: new Date()
})
},
removeNotification(id) {
globalState.notifications = globalState.notifications.filter(
notification => notification.id !== id
)
}
}
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { globalState, globalActions } from './globalState'
const app = createApp(App)
// 提供全局状态和管理函数
app.provide('globalState', globalState)
app.provide('globalActions', globalActions)
app.mount('#app')