一、这是什么?为什么需要?
1.1 简单理解
-
provide(提供):爷爷组件说:"我这里有数据,子孙们随便用!"
-
inject(注入):孙子组件说:"我要用爷爷提供的数据!"
1.2 解决的问题
没有 provide/inject 时:
text
爷爷组件 → 爸爸组件 → 儿子组件 → 孙子组件
↓ ↓ ↓ ↓
数据 props props props (需要层层传递,好麻烦!)
使用 provide/inject 后:
text
爷爷组件(provide数据)
↓
孙子组件(inject数据)直接跳过中间商!
二、基础使用方法
2.1 提供数据(爷爷组件)
vue
<template>
<div>
<ParentComponent />
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue'
import ParentComponent from './ParentComponent.vue'
// 1. 准备要提供的数据
const userInfo = ref({
name: '张三',
age: 25
})
const theme = ref('dark')
// 2. 提供数据给子孙组件
provide('userInfo', userInfo)
provide('theme', theme)
</script>
2.2 注入数据(孙子组件)
vue
<template>
<div>
<h1>用户信息:{{ userInfo.name }}</h1>
<p>主题:{{ theme }}</p>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
// 注入爷爷提供的数据
const userInfo = inject('userInfo')
const theme = inject('theme')
</script>
三、TypeScript 类型安全写法
3.1 基础类型定义
vue
<script setup lang="ts">
import { ref, provide, inject } from 'vue'
// 定义接口
interface User {
name: string
age: number
email?: string
}
// 提供数据时指定类型
const userInfo = ref<User>({
name: '李四',
age: 30
})
provide('userInfo', userInfo)
// 注入时指定类型
const userInfo = inject<User>('userInfo')
</script>
3.2 更安全的写法(推荐)
vue
<script setup lang="ts">
import { inject, ref, provide } from 'vue'
interface Theme {
mode: 'light' | 'dark'
primaryColor: string
}
// 提供默认值,避免 undefined
const defaultTheme: Theme = {
mode: 'light',
primaryColor: '#1890ff'
}
const theme = ref<Theme>(defaultTheme)
provide('theme', theme)
// 注入时提供默认值
const theme = inject<Theme>('theme', defaultTheme)
3.3 使用 Symbol 避免命名冲突(高级用法)
ts
// types.ts 或 constants.ts
export const THEME_KEY = Symbol('theme') as InjectionKey<Theme>
export const USER_KEY = Symbol('user') as InjectionKey<User>
// 爷爷组件
import { THEME_KEY, USER_KEY } from './types'
provide(THEME_KEY, theme)
provide(USER_KEY, userInfo)
// 孙子组件
const theme = inject(THEME_KEY)
const userInfo = inject(USER_KEY)
四、实际项目完整示例
4.1 用户信息共享
vue
<!-- App.vue (根组件) -->
<template>
<div :class="`app ${theme}`">
<Header />
<Content />
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue'
import Header from './Header.vue'
import Content from './Content.vue'
interface AppState {
user: {
name: string
role: string
}
theme: 'light' | 'dark'
isLogin: boolean
}
const appState = ref<AppState>({
user: {
name: '王五',
role: 'admin'
},
theme: 'light',
isLogin: true
})
// 提供整个应用状态
provide('appState', appState)
// 提供修改主题的方法
const toggleTheme = () => {
appState.value.theme = appState.value.theme === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
</script>
<style>
.light { background: white; color: black; }
.dark { background: #333; color: white; }
</style>
vue
<!-- Header.vue (头部组件) -->
<template>
<header>
<h1>欢迎,{{ appState.user.name }}</h1>
<button @click="toggleTheme">
切换主题:{{ appState.theme }}
</button>
</header>
</template>
<script setup lang="ts">
import { inject } from 'vue'
interface AppState {
user: {
name: string
role: string
}
theme: 'light' | 'dark'
isLogin: boolean
}
// 注入数据和方法
const appState = inject<AppState>('appState')!
const toggleTheme = inject<() => void>('toggleTheme')!
</script>
五、响应式数据更新
5.1 响应式原理
vue
<script setup lang="ts">
import { ref, provide } from 'vue'
// 使用 ref 创建响应式数据
const count = ref(0)
// 提供响应式数据
provide('count', count)
// 在任意组件中修改,所有注入的地方都会更新
const increase = () => {
count.value++ // 修改这里,所有注入 count 的组件都会更新
}
provide('increase', increase)
</script>
5.2 在子组件中修改
vue
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increase">+1</button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
const count = inject<number>('count')!
const increase = inject<() => void>('increase')!
</script>
六、常见问题及解决方法
6.1 问题1:inject 返回 undefined
错误:
ts
const data = inject('someKey') // 可能是 undefined
console.log(data.value) // 报错!
解决:
ts
// 方法1:提供默认值
const data = inject('someKey', defaultValue)
// 方法2:类型断言
const data = inject('someKey')!
// 方法3:运行时检查
const data = inject('someKey')
if (!data) {
throw new Error('数据未提供')
}
6.2 问题2:类型不匹配
错误:
ts
const user = inject('user') // 类型是 any,不安全
解决:
ts
interface User {
name: string
age: number
}
// 明确指定类型
const user = inject<User>('user', { name: '', age: 0 })
6.3 问题3:修改了非响应式数据
错误:
ts
// 爷爷组件
provide('staticData', { count: 0 }) // 不是响应式的!
// 孙子组件
const data = inject('staticData')
data.count++ // 修改了,但不会触发更新
解决:
ts
// 使用 ref 或 reactive 创建响应式数据
const staticData = ref({ count: 0 })
provide('staticData', staticData)
七、最佳实践
7.1 代码组织建议
ts
// useProvide.ts - 抽离 provide 逻辑
import { ref, provide } from 'vue'
export function useProvideAppState() {
const appState = ref({
user: { name: '', role: '' },
theme: 'light'
})
const updateUser = (user: User) => {
appState.value.user = user
}
const toggleTheme = () => {
appState.value.theme = appState.value.theme === 'light' ? 'dark' : 'light'
}
provide('appState', appState)
provide('updateUser', updateUser)
provide('toggleTheme', toggleTheme)
return { appState, updateUser, toggleTheme }
}
7.2 在组件中使用
vue
<script setup lang="ts">
// App.vue
import { useProvideAppState } from './useProvide'
useProvideAppState()
</script>
八、什么时候用 provide/inject?
✅ 适合的场景:
-
主题切换(深色/浅色模式)
-
用户登录信息(用户名、权限)
-
多语言国际化
-
全局配置
-
复杂表单(多个子组件需要访问表单数据)
❌ 不适合的场景:
-
父子组件通信(用 props/emit)
-
简单的状态共享(用 Pinia 更好)
-
非响应式数据(用模块导出更好)
九、总结
核心要点:
-
爷爷 provide,孙子 inject - 跳过中间组件
-
记得用 ref/reactive - 保证数据是响应式的
-
TypeScript 类型安全 - 注入时指定类型和默认值
-
Symbol 避免冲突 - 大型项目推荐使用
最简单的记忆:
ts
// 提供数据
const data = ref('你好')
provide('key', data)
// 使用数据
const data = inject('key')
这样就能在任意层级的组件中共享数据了!