1. 基本概念
provide/inject 是 Vue3 中实现跨层级组件通信的方案,类似于 React 的 Context。它允许父组件向其所有子孙组件注入依赖,无论层级有多深。
1.1 基本语法
vue
// 提供方(父组件)
const value = ref('hello')
provide('key', value)
// 注入方(子孙组件)
const value = inject('key')
2. 基础用法
2.1 提供静态值
vue
<!-- Parent.vue -->
<script setup>
import { provide } from 'vue'
// 提供静态值
provide('theme', 'dark')
provide('language', 'zh-CN')
</script>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
// 注入值
const theme = inject('theme')
const language = inject('language')
</script>
<template>
<div :class="theme">
<p>Current Language: {{ language }}</p>
</div>
</template>
2.2 提供响应式数据
vue
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue'
const count = ref(0)
const updateCount = () => {
count.value++
}
// 提供响应式数据和更新方法
provide('count', {
count,
updateCount
})
</script>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
const { count, updateCount } = inject('count')
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="updateCount">Increment</button>
</div>
</template>
3. 进阶用法
3.1 使用 Symbol 作为 key
ts
// injection-keys.ts
export const COUNT_KEY = Symbol('count')
export const THEME_KEY = Symbol('theme')
vue
<!-- Parent.vue -->
<script setup lang="ts">
import { provide } from 'vue'
import { COUNT_KEY, THEME_KEY } from './injection-keys'
const count = ref(0)
provide(COUNT_KEY, count)
provide(THEME_KEY, 'dark')
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { COUNT_KEY, THEME_KEY } from './injection-keys'
const count = inject(COUNT_KEY)
const theme = inject(THEME_KEY)
</script>
3.2 提供默认值
vue
<script setup>
import { inject } from 'vue'
// 使用静态默认值
const theme = inject('theme', 'light')
// 使用工厂函数作为默认值
const now = inject('timestamp', () => Date.now())
</script>
3.3 只读数据
vue
<!-- Parent.vue -->
<script setup>
import { provide, ref, readonly } from 'vue'
const count = ref(0)
// 提供只读版本,防止子组件修改
provide('count', readonly(count))
// 提供更新方法
provide('updateCount', () => {
count.value++
})
</script>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
const count = inject('count')
const updateCount = inject('updateCount')
</script>
4. 实际应用场景
4.1 主题系统
vue
<!-- ThemeProvider.vue -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('theme', {
theme,
toggleTheme
})
</script>
<template>
<div :class="theme.value">
<slot></slot>
</div>
</template>
<!-- 使用组件 -->
<script setup>
import { inject } from 'vue'
const { theme, toggleTheme } = inject('theme')
</script>
<template>
<button @click="toggleTheme">
Switch to {{ theme === 'light' ? 'dark' : 'light' }} mode
</button>
</template>
4.2 多语言系统
vue
<!-- I18nProvider.vue -->
<script setup>
import { provide, ref } from 'vue'
const locale = ref('en')
const messages = {
en: {
greeting: 'Hello',
farewell: 'Goodbye'
},
zh: {
greeting: '你好',
farewell: '再见'
}
}
const t = (key) => messages[locale.value][key]
const setLocale = (lang) => {
locale.value = lang
}
provide('i18n', {
locale,
t,
setLocale
})
</script>
<!-- 使用组件 -->
<script setup>
import { inject } from 'vue'
const { t, setLocale, locale } = inject('i18n')
</script>
<template>
<div>
<select v-model="locale">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
<p>{{ t('greeting') }}</p>
</div>
</template>
4.3 状态管理
vue
<!-- Store.vue -->
<script setup>
import { provide, reactive } from 'vue'
const store = reactive({
user: null,
todos: [],
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false })
},
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
})
provide('store', store)
</script>
<!-- TodoList.vue -->
<script setup>
import { inject } from 'vue'
const store = inject('store')
</script>
<template>
<div>
<input
v-model="newTodo"
@keyup.enter="store.addTodo(newTodo)"
>
<ul>
<li
v-for="todo in store.todos"
:key="todo.id"
@click="store.toggleTodo(todo.id)"
>
{{ todo.text }}
</li>
</ul>
</div>
</template>
5. TypeScript 支持
5.1 类型定义
ts
// types.ts
export interface ThemeContext {
theme: Ref<'light' | 'dark'>
toggleTheme: () => void
}
export const ThemeSymbol = Symbol('theme')
5.2 带类型的 provide/inject
vue
<script setup lang="ts">
import { provide, inject } from 'vue'
import { ThemeContext, ThemeSymbol } from './types'
// 提供方
provide<ThemeContext>(ThemeSymbol, {
theme: ref('light'),
toggleTheme: () => { /* ... */ }
})
// 注入方
const theme = inject<ThemeContext>(ThemeSymbol)
</script>
6. 最佳实践
-
使用 Symbol 作为 key
tsconst MyKey = Symbol('my-key') provide(MyKey, value)
-
提供只读数据
tsprovide('data', readonly(data))
-
集中管理 injection key
ts// keys.ts export const THEME_KEY = Symbol('theme') export const I18N_KEY = Symbol('i18n')
-
使用组合式函数封装
ts// useTheme.ts export function useTheme() { const theme = inject(THEME_KEY) if (!theme) { throw new Error('useTheme must be used within ThemeProvider') } return theme }
7. 注意事项
-
响应性
- 确保提供响应式数据时使用 ref 或 reactive
- 注意数据的可变性和只读性
-
默认值
- 提供合理的默认值
- 考虑使用工厂函数作为默认值
-
类型安全
- 使用 TypeScript 定义接口
- 使用 Symbol 作为 key
-
性能考虑
- 避免提供过大的数据结构
- 合理划分提供的数据范围
通过合理使用 provide/inject,我们可以有效地管理跨组件通信,构建可维护的组件树。但要注意避免过度使用,以免造成数据流向难以追踪的问题。