【Vue】通信组件

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>
相关推荐
若梦plus21 分钟前
Nuxt.js基础与进阶
前端·vue.js
樱花开了几轉27 分钟前
React中为甚么强调props的不可变性
前端·javascript·react.js
风清云淡_A27 分钟前
【REACT18.x】CRA+TS+ANTD5.X实现useImperativeHandle让父组件修改子组件的数据
前端·react.js
小飞大王66628 分钟前
React与Rudex的合奏
前端·react.js·前端框架
若梦plus1 小时前
React之react-dom中的dom-server与dom-client
前端·react.js
若梦plus1 小时前
react-router-dom中的几种路由详解
前端·react.js
若梦plus1 小时前
Vue服务端渲染
前端·vue.js
Mr...Gan1 小时前
VUE3(四)、组件通信
前端·javascript·vue.js
OEC小胖胖1 小时前
渲染篇(二):解密Diff算法:如何用“最少的操作”更新UI
前端·算法·ui·状态模式·web
万少1 小时前
AI编程神器!Trae+Claude4.0 简单配置 让HarmonyOS开发效率飙升 - 坚果派
前端·aigc·harmonyos