1. Props 和自定义事件(父子通信)
1.1 父传子(Props)
<!-- 父组件 -->
<template>
<child-component
:message="message"
:user-info="userInfo"
/>
</template>
<script setup>
import { ref, reactive } from 'vue'
const message = ref('Hello from parent')
const userInfo = reactive({
name: 'John',
age: 30
})
</script>
<!-- 子组件 -->
<template>
<div>
<p>{{ message }}</p>
<p>{{ userInfo.name }}</p>
</div>
</template>
<script setup>
defineProps({
message: String,
userInfo: Object
})
</script>
1.2 子传父(自定义事件)
<!-- 子组件 -->
<template>
<button @click="handleClick">更新父组件</button>
</template>
<script setup>
const emit = defineEmits(['update', 'delete'])
const handleClick = () => {
emit('update', { id: 1, value: 'new value' })
}
</script>
<!-- 父组件 -->
<template>
<child-component
@update="handleUpdate"
@delete="handleDelete"
/>
</template>
<script setup>
const handleUpdate = (data) => {
console.log('Received from child:', data)
}
const handleDelete = () => {
// 处理删除逻辑
}
</script>
2. 祖孙组件通信
2.1 provide/inject(跨层级通信)
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// 提供响应式数据和更新方法
provide('theme', {
theme,
updateTheme
})
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const { theme, updateTheme } = inject('theme')
// 使用注入的数据和方法
const toggleTheme = () => {
updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>
2.2 $attrs(透传属性)
<!-- 祖先组件 -->
<template>
<middle-component
msg="Hello"
:user-name="userName"
@custom-event="handleEvent"
/>
</template>
<!-- 中间子组件 middle-component-->
<template>
<child-component v-bind="$attrs" />
</template>
<script setup>
// 禁用 attrs 继承
defineOptions({
inheritAttrs: false
})
</script>
<!-- 孙组件 child-component -->
<template>
<div>
<p>{{ $attrs.msg }}</p>
<p>{{ $attrs.userName }}</p>
</div>
</template>
3. 兄弟组件通信
3.1 通过父组件中转
<!-- 父组件 -->
<template>
<div>
<sibling-a @message="handleMessage" />
<sibling-b :message="message" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
const handleMessage = (value) => {
message.value = value
}
</script>
<!-- 兄弟组件 A -->
<template>
<button @click="sendMessage">发送消息</button>
</template>
<script setup>
const emit = defineEmits(['message'])
const sendMessage = () => {
emit('message', 'Hello from sibling A')
}
</script>
<!-- 兄弟组件 B -->
<template>
<p>{{ message }}</p>
</template>
<script setup>
defineProps(['message'])
</script>
3.2 使用 mitt 事件总线
// eventBus.ts
import mitt from 'mitt'
export const emitter = mitt()
<!-- 组件 A -->
<script setup>
import { emitter } from './eventBus'
const sendMessage = () => {
emitter.emit('message', { text: 'Hello from A' })
}
</script>
<!-- 组件 B -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'
const handleMessage = (data) => {
console.log('Received:', data)
}
onMounted(() => {
emitter.on('message', handleMessage)
})
onUnmounted(() => {
emitter.off('message', handleMessage)
})
</script>
4. ref 和 defineExpose
4.1 父组件访问子组件
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
// 暴露属性和方法给父组件
defineExpose({
count,
increment
})
</script>
<!-- 父组件 -->
<template>
<child-component ref="childRef" />
<button @click="accessChild">访问子组件</button>
</template>
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
const accessChild = () => {
console.log(childRef.value.count)
childRef.value.increment()
}
</script>
4.2 $parent 访问父组件
<!-- 子组件 -->
<template>
<button @click="accessParent">访问父组件</button>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const accessParent = () => {
// 访问父组件的数据或方法
console.log(proxy.$parent.parentData)
proxy.$parent.parentMethod()
}
</script>
5. 插槽通信
5.1 默认插槽
<!-- 父组件 -->
<template>
<layout-component>
<p>这是默认插槽内容</p>
</layout-component>
</template>
<!-- 子组件 -->
<template>
<div class="container">
<slot></slot>
</div>
</template>
5.2 具名插槽
<!-- 父组件 -->
<template>
<layout-component>
<template #header>
<h1>页面标题</h1>
</template>
<template #default>
<main>主要内容</main>
</template>
<template #footer>
<footer>页脚内容</footer>
</template>
</layout-component>
</template>
<!-- 子组件 -->
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
5.3 作用域插槽
<!-- 子组件 -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
{{ item.name }}
</slot>
</li>
</ul>
</template>
<script setup>
defineProps(['items'])
</script>
<!-- 父组件 -->
<template>
<list-component :items="items">
<template #default="{ item, index }">
<div class="item">
<span>{{ index + 1 }}.</span>
<strong>{{ item.name }}</strong>
<p>{{ item.description }}</p>
</div>
</template>
</list-component>
</template>
6. 最佳实践
-
选择合适的通信方式
- 父子组件:优先使用 props/emit
- 跨层级组件:考虑 provide/inject
- 兄弟组件:兄弟组件通信可以使用事件总线或通过父组件中转
- 临时引用:使用 ref/expose
-
数据流向管理
- 保持单向数据流
- 避免过度使用全局状态
- 合理使用响应式数据
-
性能优化
- 避免不必要的组件通信
- 合理使用计算属性和缓存
- 及时清理事件监听器
-
代码可维护性
- 明确的命名规范
- 清晰的数据流向
- 适当的组件解耦
总结
- Props 和自定义事件(父子通信)
- 父传子:使用 props
- 子传父:使用自定义事件(emit)
- 祖孙组件通信
- provide/inject:跨层级数据传递
- $attrs:属性透传
- 兄弟组件通信
- 通过父组件中转
- 使用 mitt 事件总线
- ref 和 defineExpose
- 父组件访问子组件
- $parent 访问父组件
- 插槽通信
- 默认插槽
- 具名插槽
- 作用域插槽
- 最佳实践
- 选择合适的通信方式
- 数据流向管理
- 性能优化
- 代码可维护性