🔄 Vue3组件通信完全指南
NOTE
本文详细介绍Vue3中所有组件通信方式,包含最佳实践和使用场景!
📊 通信方式总览
mindmap root((Vue3组件通信)) Props/Emit 父子组件 单向数据流 Provide/Inject 跨层级通信 依赖注入 Event Bus 事件总线 全局事件 Vuex/Pinia 状态管理 全局状态 Refs 直接访问 组件实例 v-model 双向绑定 组件绑定
🎯 Props/Emit
1. 基础使用
父组件传递数据给子组件
vue
<!-- 父组件 Parent.vue -->
<template>
<child-component
:message="message"
:user-info="userInfo"
@update="handleUpdate"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 定义数据
const message = ref('Hello from parent')
const userInfo = ref({
name: 'John',
age: 30
})
// 处理子组件事件
const handleUpdate = (newValue: string) => {
console.log('Updated:', newValue)
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
<p>{{ userInfo.name }}</p>
<button @click="updateParent">Update</button>
</div>
</template>
<script setup lang="ts">
// 定义props
const props = defineProps<{
message: string
userInfo: {
name: string
age: number
}
}>()
// 定义emit
const emit = defineEmits<{
(e: 'update', value: string): void
}>()
const updateParent = () => {
emit('update', 'New Value')
}
</script>
2. Props验证
typescript
// 使用运行时声明
defineProps({
propA: String,
propB: {
type: Number,
required: true,
default: 100,
validator: (value: number) => value > 0
}
})
// 使用类型声明(推荐)
defineProps<{
propA: string
propB: number
propC?: boolean
}>()
// 带默认值的类型声明
withDefaults(defineProps<{
propA: string
propB: number
propC?: boolean
}>(), {
propC: false
})
🌟 Provide/Inject
1. 基础使用
vue
<!-- 父组件或根组件 -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const theme = ref('dark')
const updateTheme = (newTheme: string) => {
theme.value = newTheme
}
// 提供数据和方法
provide('theme', {
theme,
updateTheme
})
</script>
<!-- 子组件(任意层级) -->
<script setup lang="ts">
import { inject } from 'vue'
// 注入数据和方法
const { theme, updateTheme } = inject('theme', {
theme: ref('light'),
updateTheme: (theme: string) => {}
})
</script>
2. 类型安全的Provide/Inject
typescript
// 定义注入键类型
type ThemeKey = 'theme'
// 定义注入值类型
interface ThemeContext {
theme: Ref<string>
updateTheme: (theme: string) => void
}
// 提供者组件
provide<ThemeContext>(ThemeKey, {
theme: ref('dark'),
updateTheme: (newTheme) => {}
})
// 注入者组件
const theme = inject<ThemeContext>(ThemeKey)
⚡ Event Bus
1. 使用mitt实现事件总线
typescript
// eventBus.ts
import mitt from 'mitt'
type Events = {
'user:login': { id: number; name: string }
'user:logout': void
}
export const eventBus = mitt<Events>()
// 组件A
import { eventBus } from './eventBus'
// 发送事件
eventBus.emit('user:login', {
id: 1,
name: 'John'
})
// 组件B
import { eventBus } from './eventBus'
// 监听事件
eventBus.on('user:login', (user) => {
console.log('User logged in:', user)
})
// 清理事件
onUnmounted(() => {
eventBus.off('user:login')
})
💾 Vuex/Pinia
1. Pinia示例
typescript
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
isLoggedIn: false
}),
getters: {
userStatus: (state) =>
state.isLoggedIn ? 'Logged In' : 'Guest'
},
actions: {
login(name: string) {
this.name = name
this.isLoggedIn = true
},
logout() {
this.name = ''
this.isLoggedIn = false
}
}
})
// 组件中使用
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 使用状态
console.log(userStore.name)
// 使用getter
console.log(userStore.userStatus)
// 使用action
userStore.login('John')
</script>
🎯 Refs直接访问
1. 模板引用
vue
<template>
<child-component ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 定义ref
const childRef = ref<InstanceType<typeof ChildComponent>>()
onMounted(() => {
// 访问子组件实例
childRef.value?.someMethod()
})
</script>
🔄 v-model组件绑定
1. 基础使用
vue
<!-- 父组件 -->
<template>
<custom-input
v-model="searchText"
v-model:title="title"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const searchText = ref('')
const title = ref('')
</script>
<!-- 子组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
<input
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
<script setup lang="ts">
defineProps<{
modelValue: string
title: string
}>()
defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'update:title', value: string): void
}>()
</script>
📝 最佳实践建议
1. 选择通信方式的考虑因素
graph TD A[组件关系] --> B{是否父子组件?} B -->|是| C[Props/Emit] B -->|否| D{是否跨多层?} D -->|是| E[Provide/Inject] D -->|否| F{是否全局状态?} F -->|是| G[Pinia] F -->|否| H[Event Bus]
2. 性能优化建议
typescript
// 1. Props解构优化
// 不推荐
const { title } = props
watch(title, () => {})
// 推荐
watch(() => props.title, () => {})
// 2. 提供响应式数据
// 不推荐
provide('theme', theme.value)
// 推荐
provide('theme', readonly(theme))
// 3. 合理使用事件总线
// 及时清理事件监听
onUnmounted(() => {
eventBus.all.clear()
})
📚 扩展阅读
🎯 补充:Vue3组合式API问答指南
1. 基础概念类问题
Q1: 什么是组合式API?为什么要使用它?
答题思路:先说概念,再说解决的问题,最后说优势
markdown
1. 概念:
- 组合式API是Vue3引入的一种新的组件逻辑组织方式
- 它允许我们使用函数而不是选项来组织组件逻辑
2. 解决的问题:
- 选项式API中代码逻辑分散
- 代码复用困难
- 类型推导不友好
3. 主要优势:
- 更好的逻辑复用
- 更灵活的代码组织
- 更好的类型推导
- 更小的打包体积(得益于tree-shaking)
Q2: setup函数的作用是什么?
markdown
1. 核心功能:
- 组合式API的入口点
- 在组件创建之前执行
- 返回的对象会暴露给模板
2. 特点:
- 没有this上下文
- 可以访问props和context
- 可以是异步函数
3. 使用方式:
- <script setup> 语法糖(推荐)
- 手动return暴露数据
2. 响应式系统问题
Q1: ref和reactive的区别?
typescript
// 答题示例:
1. 使用场景不同:
- ref主要用于基本类型
const count = ref(0)
- reactive用于对象类型
const state = reactive({ count: 0 })
2. 访问方式不同:
- ref需要.value
console.log(count.value)
- reactive直接访问
console.log(state.count)
3. 解构行为不同:
- ref保持响应性
const { count } = useCount() // count是ref
- reactive解构会失去响应性
const { user } = userState // 失去响应性
Q2: 如何处理响应式数据的常见问题?
typescript
// 1. 解构响应式对象
// ❌ 错误方式
const { name, age } = reactive({ name: 'John', age: 30 })
// ✅ 正确方式
const state = reactive({ name: 'John', age: 30 })
const { name, age } = toRefs(state)
// 2. 数组响应式
// ❌ 错误方式
const list = reactive([1, 2, 3])
list[0] = 4 // 可能不会触发更新
// ✅ 正确方式
const list = ref([1, 2, 3])
list.value[0] = 4
3. 生命周期问题
Q1: 组合式API中的生命周期钩子有哪些?
typescript
// 答题示例:
1. 主要钩子函数:
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
2. 特殊钩子:
- onErrorCaptured
- onRenderTracked
- onRenderTriggered
3. 使用示例:
onMounted(() => {
// 组件挂载后执行
fetchData()
})
onUnmounted(() => {
// 清理工作
clearInterval(timer)
})
4. 实践经验问题
Q1: 如何使用组合式API实现代码复用?
typescript
// 答题示例:
// 1. 封装组合式函数
function useCounter() {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
return {
count,
increment,
decrement
}
}
// 2. 在组件中使用
export default {
setup() {
const { count, increment } = useCounter()
return {
count,
increment
}
}
}
Q2: 组合式API中如何处理异步操作?
typescript
// 答题示例:
function useAsyncData() {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async () => {
loading.value = true
try {
data.value = await api.getData()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
}
5. 性能优化问题
Q1: 如何优化组合式API的性能?
typescript
// 答题要点:
1. 计算属性缓存:
const cachedValue = computed(() => {
return expensiveOperation(props.value)
})
2. 避免不必要的响应式:
// 大数据使用shallowRef
const bigData = shallowRef(largeArray)
3. 及时清理:
onUnmounted(() => {
// 清理订阅、定时器等
})
4. 合理使用watchEffect:
// 自动收集依赖
watchEffect(() => {
// 只有依赖变化时才执行
})
📝 答题技巧
结构化回答:
- 先说概念
- 再说用法
- 最后举例
突出重点:
- 核心特性
- 最佳实践
- 实际经验
展示深度:
- 提及原理
- 说明优化方案
- 分享踩坑经验
准备示例:
- 准备简单代码示例
- 准备实际项目案例
- 准备性能优化案例