1. Props / Emit (父子组件通信)
Props (父传子)
- 父组件通过
v-bind
或:
传递数据给子组件 - 子组件通过
defineProps
接收数据 - 案例
Parent.vue
<!-- Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<Child :message="parentMessage" :count="number" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const parentMessage = ref('Hello from parent') const number = ref(42)
</script>
Child.vue
<!-- Child.vue -->
<template>
<div>
<h3>子组件</h3>
<p>接收到的消息: {{ message }}</p>
<p>接收到的数字: {{ count }}</p>
</div>
</template>
<script setup lang="ts">
// 定义props类型
interface Props { message: string count: number } // 接收父组件传递的props
const props = defineProps<Props>()
</script>
Emit (子传父)
- 父组件通过
v-bind
或:
传递数据给子组件 - 子组件通过
defineProps
接收数据 - 案例
- 子组件通过
defineEmits
定义事件 - 父组件通过
@事件名
监听子组件事件 - 案例
Parent.vue
<!-- Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<p>从子组件接收到的数据: {{ childData }}</p>
<Child @child-event="handleChildEvent" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const childData = ref('')
// 处理子组件触发的事件
const handleChildEvent = (data: string) => {
childData.value = data
}
</script>
Child.vue
<!-- Child.vue -->
<template>
<div>
<h3>子组件</h3>
<button @click="sendDataToParent">发送数据给父组件</button>
</div>
</template>
<script setup lang="ts">
// 定义要触发的事件
const emit = defineEmits<{
(e: 'child-event', data: string): void
}>()
const sendDataToParent = () => {
emit('child-event', '这是来自子组件的数据')
}
</script>
双向通信
Parent.vue
<!-- Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<p>父组件数据: {{ parentData }}</p>
<button @click="updateParentData">更新父组件数据</button>
<!-- 传递数据给子组件,并监听子组件事件 -->
<Child
:title="childTitle"
:content="childContent"
@update-content="handleContentUpdate"
@notify-parent="handleNotification"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const parentData = ref('父组件原始数据')
const childTitle = ref('子组件标题')
const childContent = ref('传递给子组件的内容')
const updateParentData = () => {
parentData.value = '父组件数据已更新'
}
const handleContentUpdate = (newContent: string) => {
childContent.value = newContent
}
const handleNotification = (message: string) => {
alert(`收到子组件通知: ${message}`)
}
</script>
Child.vue
<!-- Child.vue -->
<template>
<div>
<h3>{{ title }}</h3>
<p>{{ content }}</p>
<input v-model="inputValue" placeholder="输入要传递给父组件的内容" />
<button @click="updateContent">更新内容</button>
<button @click="notifyParent">通知父组件</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
title: string
content: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update-content', value: string): void
(e: 'notify-parent', message: string): void
}>()
const inputValue = ref('')
const updateContent = () => {
emit('update-content', inputValue.value)
}
const notifyParent = () => {
emit('notify-parent', '子组件发来通知')
}
</script>
2. Provide / Inject (跨层级通信)
- Provide/Inject 是 Vue 中用于跨层级组件通信的机制,允许祖先组件向其所有子孙组件提供数据,而不需要通过中间组件逐层传递。
Provide
- 祖先组件通过
provide
提供数据 - 可以传递响应式数据
Inject
- 后代组件通过
inject
注入数据 - 可以设置默认值
案例
- 基础案例
App.vue
<!-- App.vue 祖先组件 -->
<template>
<div>
<h1>祖先组件</h1>
<Parent />
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue'
import Parent from './Parent.vue'
// 提供响应式数据
const theme = ref('dark')
const userName = ref('张三')
// 提供方法
const updateTheme = (newTheme: string) =>
{
theme.value = newTheme
}
// 使用 provide 提供数据和方法
provide('theme', theme)
provide('userName', userName) provide('updateTheme', updateTheme)
</script>
任意子孙.vue
<!-- Child.vue 子孙组件 -->
<template>
<div>
<h3>子孙组件</h3>
<p>当前主题: {{ currentTheme }}</p>
<p>用户名: {{ name }}</p>
<button @click="changeTheme">切换主题</button>
</div>
</template>
<script setup lang="ts">
import { inject, Ref } from 'vue'
// 注入祖先组件提供的数据
const currentTheme = inject('theme') as Ref<string>
const name = inject('userName') as Ref<string>
const updateTheme = inject('updateTheme') as (theme: string) => void
const changeTheme = () => {
updateTheme(currentTheme.value === 'dark' ? 'light' : 'dark')
}
</script>
3. Event Bus (事件总线)
使用 mitt 库
- 适用于任意组件间通信
- 需要手动管理事件监听器的清理
- Event Bus 是一种组件间通信模式,允许任何组件之间进行通信,而不需要通过父子关系。它创建一个中央事件系统,组件可以向其发送或监听事件。
案例
eventBus.ts
// eventBus.ts
import { mitt } from 'mitt'
// 创建事件总线实例
const eventBus = mitt()
export default eventBus
ComponentA.vue
<!-- ComponentA.vue -->
<template>
<div>
<h3>组件 A</h3>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
<button @click="sendMessage">发送消息给组件B</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import eventBus from './eventBus'
const count = ref(0)
const increment = () => {
count.value++
// 发送事件到事件总线
eventBus.emit('count-changed', count.value)
}
const sendMessage = () => {
eventBus.emit('message-to-b', {
text: '来自组件A的消息',
timestamp: new Date().toLocaleTimeString()
})
}
</script>
ComponentB.vue
<!-- ComponentB.vue -->
<template>
<div>
<h3>组件 B</h3>
<p>接收到的计数: {{ receivedCount }}</p>
<p>消息: {{ message.text }}</p>
<p>时间: {{ message.timestamp }}</p>
<button @click="sendResponse">回复组件A</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus from './eventBus'
const receivedCount = ref(0)
const message = ref({ text: '', timestamp: '' })
// 监听事件总线上的事件 onMounted(() => {
eventBus.on('count-changed', (count: number) => {
receivedCount.value = count
})
eventBus.on('message-to-b', (data: { text: string; timestamp: string }) => {
message.value = data
})
})
// 移除事件监听器
onUnmounted(() => {
eventBus.off('count-changed')
eventBus.off('message-to-b')
})
const sendResponse = () => {
eventBus.emit('response-to-a', '组件B已收到消息')
}
</script>
ComponentC.vue
<!-- ComponentC.vue -->
<template>
<div>
<h3>组件 C</h3>
<p>回复消息: {{ response }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus from './eventBus'
const response = ref('')
onMounted(() => {
eventBus.on('response-to-a',
(message: string) => {
response.value = message
})
})
onUnmounted(() => {
eventBus.off('response-to-a')
})
</script>
App.vue
<!-- App.vue -->
<template>
<div>
<h1>Event Bus 示例</h1>
<ComponentA />
<ComponentB />
<ComponentC />
</div>
</template>
<script setup lang="ts">
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
import ComponentC from './ComponentC.vue'
</script>
4. Pinia (状态管理) (Vue3)
Store
- 全局状态管理
- 支持响应式数据
- 支持持久化
stores/counter.ts
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state
state: () => ({
count: 0,
name: '计数器'
}),
// getters
getters: {
doubleCount: (state) => state.count * 2,
formattedName: (state) => `当前${state.name}: ${state.count}`
},
// actions
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementBy(amount: number) {
this.count += amount
},
reset() {
this.count = 0
}
}
})
CounterComponents.vue
<!-- CounterComponent.vue -->
<template>
<div>
<h3>计数器组件</h3>
<p>计数: {{ counter.count }}</p>
<p>双倍计数: {{ counter.doubleCount }}</p>
<p>格式化名称: {{ counter.formattedName }}</p>
<button @click="counter.increment">增加</button>
<button @click="counter.decrement">减少</button>
<button @click="incrementByFive">增加5</button>
<button @click="counter.reset">重置</button>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
// 使用 store
const counter = useCounterStore()
// 在组件中调用 action
const incrementByFive = () => {
counter.incrementBy(5)
}
</script>
DisplayComponents.vue
<!-- DisplayComponent.vue -->
<template>
<div>
<h3>显示组件</h3>
<p>计数: {{ counter.count }}</p>
<p>双倍计数: {{ counter.doubleCount }}</p>
<button @click="counter.increment">从这里增加</button>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
5. Vuex (Vue 2)
store/index.js
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 状态
state: {
count: 0,
message: 'Hello Vuex'
},
// 计算属性
getters: {
doubleCount: state => state.count * 2,
formattedMessage: state => `${state.message} - 计数: ${state.count}`
},
// 同步修改状态
mutations: {
INCREMENT(state) {
state.count++
},
DECREMENT(state) {
state.count--
},
SET_COUNT(state, payload) {
state.count = payload
},
SET_MESSAGE(state, payload) {
state.message = payload
}
},
// 异步操作 actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('INCREMENT')
}, 1000)
},
setCountAsync({ commit }, value) {
return new Promise((resolve) => {
setTimeout(() => {
commit('SET_COUNT', value)
resolve()
}, 500)
})
}
}
})
Counter.vue
<!-- Counter.vue -->
<template>
<div>
<h3>计数器组件</h3>
<p>计数: {{ count }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<p>格式化消息: {{ formattedMessage }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="incrementAsync">异步增加</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
// 映射 state 到计算属性
...mapState(['count', 'message']),
// 映射 getters 到计算属性
...mapGetters(['doubleCount', 'formattedMessage'])
},
methods: {
// 映射 mutations 到方法
...mapMutations(['INCREMENT', 'DECREMENT', 'SET_COUNT']),
// 映射 actions 到方法
...mapActions(['incrementAsync']),
reset() {
this.SET_COUNT(0)
}
}
}
</script>
Display.vue
<!-- Display.vue -->
<template>
<div>
<h3>显示组件</h3>
<p>计数: {{ count }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<button @click="INCREMENT">从这里增加</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['INCREMENT'])
}
}
</script>
推荐使用 Pinia
- 更简洁的 API
- 更好的 TypeScript 支持
- 更小的包体积
6. 其他方式
ref / reactive
- 通过 ref 引用直接访问组件实例
- 适用于特定场景
案例
Parent.vue
<!-- Parent.vue -->
<template>
<div>
<h2>父组件</h2>
<p>父组件数据: {{ parentData }}</p>
<!-- 给子组件添加 ref 引用 -->
<ChildComponent
ref="childRef"
:message="childMessage"
@child-event="handleChildEvent"
/>
<div class="actions">
<button @click="callChildMethod">调用子组件方法</button>
<button @click="getChildData">获取子组件数据</button>
<button @click="updateChildData">更新子组件数据</button>
</div>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
data() {
return {
parentData: '父组件数据',
childMessage: '传递给子组件的消息'
}
},
methods: {
// 调用子组件的方法
callChildMethod() {
// 通过 $refs 访问子组件实例
this.$refs.childRef.childMethod('来自父组件的参数')
},
// 获取子组件的数据
getChildData() {
const childData = this.$refs.childRef.childData
console.log('子组件数据:', childData)
alert(`子组件数据: ${childData}`)
},
// 更新子组件的数据
updateChildData() {
this.$refs.childRef.childData = '父组件更新的数据'
},
// 处理子组件触发的事件
handleChildEvent(data) {
console.log('接收到子组件事件:', data)
this.parentData = data
}
}
}
</script>
ChildComponnet.vue
<!-- ChildComponent.vue -->
<template>
<div>
<h3>子组件</h3>
<p>接收到的消息: {{ message }}</p>
<p>子组件数据: {{ childData }}</p>
<div class="actions">
<button @click="emitEvent">触发事件给父组件</button>
<button @click="localMethod">执行本地方法</button>
</div>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
},
data() {
return {
childData: '子组件原始数据',
counter: 0
}
},
methods: {
// 子组件的方法,可以被父组件调用
childMethod(param) {
this.counter++
console.log(`子组件方法被调用 ${this.counter} 次,参数:`, param)
this.childData = `方法调用 ${this.counter} 次`
},
// 子组件的本地方法
localMethod() {
alert('子组件本地方法执行')
},
// 触发事件给父组件
emitEvent() {
this.$emit('child-event', '子组件发送的数据')
}
}
}
</script>
插槽 (Slots)
- 父组件向子组件传递内容
- 支持作用域插槽
1、基础插槽
Card.vue
<!-- Card.vue 子组件 -->
<template>
<div>
<header>
<slot name="header">
<h3>默认标题</h3>
</slot>
</header>
<main>
<slot>
<p>默认内容</p>
</slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
Parent.vue
<!-- Parent.vue 父组件 -->
<template>
<div>
<!-- 使用默认插槽内容 -->
<Card />
<!-- 自定义插槽内容 -->
<Card>
<template #header>
<h2>自定义标题</h2>
</template>
<div>
<p>这是自定义的内容</p>
<p>可以是任意模板</p>
</div>
<template #footer>
<button @click="handleAction">操作按钮</button>
</template>
</Card>
</div>
</template>
<script setup>
import Card from './Card.vue'
const handleAction = () => {
console.log('按钮被点击')
}
</script>
2、作用域插槽
DataTable.vur
<!-- DataTable.vue 子组件 -->
<template>
<div>
<div v-for="(item, index) in data" :key="index">
<!-- 作用域插槽,向父组件传递数据 -->
<slot
:item="item"
:index="index"
:isFirst="index === 0"
>
<!-- 默认显示 -->
<span>{{ item }}</span>
</slot>
</div>
</div>
</template>
<script setup>
defineProps({
data: {
type: Array,
default: () => []
}
})
</script>
Parent.vue
<!-- Parent.vue 父组件 -->
<template>
<div>
<DataTable :data="items">
<!-- 作用域插槽,接收子组件传递的数据 -->
<template #default="{ item, index, isFirst }">
<div>
<span>{{ index + 1 }}.</span>
<strong v-if="isFirst">{{ item }}</strong>
<span v-else>{{ item }}</span>
</div>
</template>
</DataTable>
</div>
</template>
<script setup>
import DataTable from './DataTable.vue'
const items = ['苹果', '香蕉', '橙子', '葡萄']
</script>