Vue3的组件通信方式相比Vue2有了显著变化。Composition API的引入使得跨组件状态共享更加灵活,但同时也增加了方案选型的复杂度。不同场景下应选择不同的通信方式,避免过度使用全局状态管理工具。
父子组件通信:defineProps与defineEmits
父子组件通信是最基础的通信模式。Vue3的script setup语法糖提供了编译器宏defineProps和defineEmits,简化了Props和Events的声明方式。
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const message = ref('Hello from parent')
const count = ref(0)
function handleUpdate(newValue) {
count.value = newValue
}
</script>
<template>
<Child
:message="message"
:count="count"
@update="handleUpdate"
/>
</template>
<!-- Child.vue -->
<script setup>
const props = defineProps({
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update'])
function increment() {
emit('update', props.count + 1)
}
</script>
<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
使用v-model实现双向绑定时,Vue3的语法与Vue2不同。Vue3的v-model默认绑定prop名为modelValue,事件名为update:modelValue。一个组件可以绑定多个v-model。
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const username = ref('')
const email = ref('')
</script>
<template>
<CustomInput
v-model="username"
v-model:email="email"
/>
</template>
<!-- CustomInput.vue -->
<script setup>
const props = defineProps({
modelValue: String,
email: String
})
const emit = defineEmits([
'update:modelValue',
'update:email'
])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
function onEmailInput(e) {
emit('update:email', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="onInput" placeholder="用户名" />
<input :value="email" @input="onEmailInput" placeholder="邮箱" />
</template>
跨层级通信:provide与inject
当组件嵌套层级较深时,通过props逐层传递数据(prop drilling)代码冗余且维护困难。provide/inject允许祖先组件向所有后代组件注入数据,跳过中间层级。
<!-- GrandParent.vue -->
<script setup>
import { ref, provide, readonly } from 'vue'
import Parent from './Parent.vue'
const theme = ref('dark')
const userInfo = ref({ name: '张三', role: 'admin' })
// provide的值可以是响应式引用
// 使用readonly防止后代组件直接修改
provide('theme', theme)
provide('userInfo', readonly(userInfo))
// 提供修改方法而非直接暴露可变引用
function updateTheme(newTheme) {
theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>
<template>
<Parent />
</template>
<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue'
// 第二个参数是默认值,当没有provider提供时使用
const theme = inject('theme', 'light')
const userInfo = inject('userInfo', { name: '', role: 'guest' })
const updateTheme = inject('updateTheme', () => {})
// 尝试修改readonly的值会告警
// userInfo.value.name = '李四' // Vue warning: Set operation on key "name" failed
</script>
<template>
<div :class="theme">
<p>用户: {{ userInfo.name }} ({{ userInfo.role }})</p>
<button @click="updateTheme('light')">切换主题</button>
</div>
</template>
provide/inject的数据流是单向的,从上到下。通过readonly包装注入的数据并提供修改方法,可以保持数据流的清晰性。这种模式本质上是手动实现的依赖注入,适合主题切换、国际化、用户信息等全局性配置。
兄弟组件通信:事件总线与状态共享
Vue3移除了实例的$on、$emit、$off方法,不再支持通过组件实例创建事件总线。兄弟组件通信的推荐方案是使用第三方库mitt或直接共享响应式状态。
// eventBus.js - 使用mitt创建事件总线
import mitt from 'mitt'
const bus = mitt()
export default bus
// ComponentA.vue - 发送事件
<script setup>
import bus from './eventBus'
function sendData() {
bus.emit('data-updated', { id: 1, name: '新数据' })
}
</script>
// ComponentB.vue - 接收事件
<script setup>
import { onUnmounted, ref } from 'vue'
import bus from './eventBus'
const receivedData = ref(null)
bus.on('data-updated', (data) => {
receivedData.value = data
})
// 组件卸载时移除监听,避免内存泄漏
onUnmounted(() => {
bus.off('data-updated')
})
</script>
<template>
<p>收到的数据: {{ receivedData }}</p>
</template>
对于需要多组件共享的复杂状态,使用响应式对象共享比事件总线更易于维护。创建一个独立的composable函数封装状态和操作方法,任何组件都可以导入使用。
// useCart.js - 共享购物车状态
import { ref, computed, readonly } from 'vue'
// 模块级别的响应式状态,所有组件共享同一实例
const items = ref([])
const isOpen = ref(false)
export function useCart() {
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const count = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
function addItem(product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(id) {
items.value = items.value.filter(i => i.id !== id)
}
function toggle() {
isOpen.value = !isOpen.value
}
return {
items: readonly(items),
isOpen: readonly(isOpen),
total,
count,
addItem,
removeItem,
toggle
}
}
// 任何组件中使用
// import { useCart } from './useCart'
// const { items, total, addItem } = useCart()
使用provide/inject实现类型安全的依赖注入
在TypeScript项目中,provide/inject默认返回any类型,失去了类型检查的优势。通过InjectionKey可以实现类型安全的依赖注入。
// keys.ts
import { InjectionKey, Ref } from 'vue'
interface AppContext {
theme: Ref<string>;
user: Ref<{ name: string; role: string }>;
updateTheme: (theme: string) => void;
}
// 使用InjectionKey定义注入的类型
export const AppContextKey: InjectionKey<AppContext> = Symbol('app-context')
// Provider.vue
import { provide } from 'vue'
import { AppContextKey } from './keys'
provide(AppContextKey, {
theme,
user: readonly(user),
updateTheme
})
// Consumer.vue
import { inject } from 'vue'
import { AppContextKey } from './keys'
// inject会推断出AppContext类型
const ctx = inject(AppContextKey)
if (!ctx) throw new Error('AppContext not provided')
// 完整的TypeScript类型提示
ctx.theme.value // string
ctx.user.value // { name: string; role: string }
ctx.updateTheme // (theme: string) => void
类型安全的依赖注入在大型项目中尤为重要。组件库设计场景下,通过InjectionKey暴露组件API,消费方可以获得完整的IDE类型提示。响应式布局相关的全局配置(如断点阈值、栅格列数)也适合通过provide/inject注入,避免在每个组件中硬编码。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-tong-xin-quan-fang-an-provideinject-yu-zi-ding/