在现代前端开发中,组件化已成为构建复杂应用的核心思想。Vue作为一款流行的渐进式JavaScript框架,提供了多种灵活的组件通信方式。本文将详细介绍Vue中7种常用的组件通信方法,并通过实际示例演示它们的应用场景和使用技巧。
组件通信方式概览
在Vue应用中,组件通信主要有以下7种方式:
props- 父组件向子组件传递数据emit- 子组件向父组件发送事件v-model- 双向数据绑定语法糖refs- 直接访问组件实例或DOM元素provide/inject- 跨层级组件数据传递eventBus- 全局事件总线vuex/pinia- 状态管理工具
下面我们将重点介绍前5种方式,并通过一个简单的待办事项列表示例来演示它们的实际应用。
示例场景说明
在我们的示例中,将实现一个简单的待办事项应用:
- 父组件:显示任务列表
- 子组件:包含输入框和添加按钮
1. Props方式 - 父传子
Props是Vue中最常见的父组件向子组件传递数据的方式。
父组件代码
<template>
<!-- 子组件 -->
<child-components :list="list"></child-components>
<!-- 父组件 -->
<div class="child-wrap input-group">
<input
v-model="value"
type="text"
class="form-control"
placeholder="请输入任务"
/>
<div class="input-group-append">
<button @click="handleAdd" class="btn btn-primary" type="button">
添加
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'
const list = ref(['JavaScript', 'HTML', 'CSS'])
const value = ref('')
// 添加任务的处理函数
const handleAdd = () => {
list.value.push(value.value)
value.value = '' // 清空输入框
}
</script>
子组件代码
<template>
<ul class="parent list-group">
<li class="list-group-item" v-for="item in props.list" :key="item">
{{ item }}
</li>
</ul>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
list: {
type: Array,
default: () => [],
},
})
</script>
2. Emit方式 - 子传父
Emit允许子组件向父组件发送事件,是实现子传父通信的标准方式。
子组件代码
<template>
<div class="child-wrap input-group">
<input
v-model="value"
type="text"
class="form-control"
placeholder="请输入任务"
/>
<div class="input-group-append">
<button @click="handleSubmit" class="btn btn-primary" type="button">
添加
</button>
</div>
</div>
</template>
<script setup>
import { ref, defineEmits } from 'vue'
const value = ref('')
const emits = defineEmits(['add']) // 定义可发出的事件
const handleSubmit = () => {
emits('add', value.value) // 发出add事件并传递值
value.value = '' // 清空输入框
}
</script>
父组件代码
<template>
<!-- 父组件 -->
<ul class="parent list-group">
<li class="list-group-item" v-for="item in list" :key="item">
{{ item }}
</li>
</ul>
<!-- 子组件 -->
<child-components @add="handleAdd"></child-components>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'
const list = ref(['JavaScript', 'HTML', 'CSS'])
// 处理添加事件
const handleAdd = (value) => {
list.value.push(value)
}
</script>
3. v-model方式 - 双向绑定
v-model是Vue提供的语法糖,可以简化双向数据绑定的实现。
子组件代码
<template>
<div class="child-wrap input-group">
<input
v-model="value"
type="text"
class="form-control"
placeholder="请输入任务"
/>
<div class="input-group-append">
<button @click="handleAdd" class="btn btn-primary" type="button">
添加
</button>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue'
const value = ref('')
const props = defineProps({
list: {
type: Array,
default: () => [],
},
})
const emits = defineEmits(['update:list']) // 固定格式事件
// 添加任务
const handleAdd = () => {
const arr = [...props.list] // 创建新数组以避免直接修改props
arr.push(value.value)
emits('update:list', arr) // 发出更新事件
value.value = '' // 清空输入框
}
</script>
父组件代码
<template>
<!-- 父组件 -->
<ul class="parent list-group">
<li class="list-group-item" v-for="item in list" :key="item">
{{ item }}
</li>
</ul>
<!-- 子组件 -->
<child-components v-model:list="list"></child-components>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'
const list = ref(['JavaScript', 'HTML', 'CSS'])
</script>
注意:
update:*是Vue中的固定写法,*表示props中的属性名。
4. Refs方式 - 直接访问
Refs允许父组件直接访问子组件的实例或方法。
父组件代码
<template>
<ul class="parent list-group">
<li class="list-group-item" v-for="item in childRefs?.list" :key="item">
{{ item }}
</li>
</ul>
<!-- 子组件 ref的值与script中的保持一致 -->
<child-components ref="childRefs"></child-components>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'
const childRefs = ref(null) // 创建与组件ref同名的响应式引用
</script>
子组件代码
<template>
<div class="child-wrap input-group">
<input
v-model="value"
type="text"
class="form-control"
placeholder="请输入任务"
/>
<div class="input-group-append">
<button @click="handleAdd" class="btn btn-primary" type="button">
添加
</button>
</div>
</div>
</template>
<script setup>
import { ref, defineExpose } from 'vue'
const list = ref(['JavaScript', 'HTML', 'CSS'])
const value = ref('')
// 添加任务
const handleAdd = () => {
list.value.push(value.value)
value.value = '' // 清空输入框
}
// 暴露数据和方法给父组件
defineExpose({
list,
handleAdd
})
</script>
注意:
<script setup>语法糖默认是关闭的,需要通过defineExposeAPI显式暴露需要公开的属性和方法。
5. Provide/Inject方式 - 跨层级传递
Provide/Inject这对API可以实现祖先组件向后代组件传递数据,无论中间隔了多少层级。
父组件代码
<template>
<!-- 子组件 -->
<child-components></child-components>
<!-- 父组件 -->
<div class="child-wrap input-group">
<input
v-model="value"
type="text"
class="form-control"
placeholder="请输入任务"
/>
<div class="input-group-append">
<button @click="handleAdd" class="btn btn-primary" type="button">
添加
</button>
</div>
</div>
</template>
<script setup>
import { ref, provide, readonly } from 'vue'
import ChildComponents from './child.vue'
const list = ref(['JavaScript', 'HTML', 'CSS'])
const value = ref('')
// 向所有后代组件提供数据
provide('list', readonly(list.value)) // 使用readonly保护数据
// 添加任务
const handleAdd = () => {
list.value.push(value.value)
value.value = '' // 清空输入框
}
</script>
子组件代码
<template>
<ul class="parent list-group">
<li class="list-group-item" v-for="item in list" :key="item">
{{ item }}
</li>
</ul>
</template>
<script setup>
import { inject } from 'vue'
// 接收祖先组件提供的数据
const list = inject('list')
</script>
最佳实践:使用provide传递数据时,建议使用
readonly包装数据,避免子组件意外修改父级数据,保持数据的单向流。
6. Event Bus 方式
eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
发布组件
<template>
<div class="container">
<h3>Event Bus方式</h3>
<task-list />
<div class="input-group">
<input v-model="taskInput" placeholder="输入任务" />
<button @click="addTask">添加</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { emitter } from './eventBus'
const taskInput = ref('')
const addTask = () => {
if (taskInput.value.trim()) {
emitter.emit('task-added', taskInput.value.trim())
taskInput.value = ''
}
}
</script>
订阅组件 TaskList.vue
<template>
<ul class="task-list">
<li v-for="(task, index) in tasks" :key="index">
{{ task }}
</li>
</ul>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'
const tasks = ref(['Node.js', 'Express', 'MongoDB'])
onMounted(() => {
emitter.on('task-added', (task) => {
tasks.value.push(task)
})
})
onUnmounted(() => {
emitter.off('task-added')
})
</script>
7. Vuex/Pinia 方式
store/taskStore.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useTaskStore = defineStore('tasks', () => {
const tasks = ref(['TypeScript', 'Webpack', 'Vite'])
const addTask = (task) => {
if (task.trim()) {
tasks.value.push(task.trim())
}
}
return { tasks, addTask }
})
使用Pinia的组件
<template>
<div class="container">
<h3>Pinia方式</h3>
<ul class="task-list">
<li v-for="(task, index) in taskStore.tasks" :key="index">
{{ task }}
</li>
</ul>
<div class="input-group">
<input v-model="taskInput" placeholder="输入任务" />
<button @click="addTask">添加</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useTaskStore } from './store/taskStore'
const taskStore = useTaskStore()
const taskInput = ref('')
const addTask = () => {
taskStore.addTask(taskInput.value)
taskInput.value = ''
}
</script>
总结与选择建议
每种通信方式都有其适用场景:
- Props - 简单的父传子数据传递,最常用
- Emit - 子组件向父组件发送事件,处理用户交互
- v-model - 简化表单元素的双向绑定,语法糖便捷
- Refs - 需要直接访问子组件实例或方法时使用
- Provide/Inject - 跨多层组件传递数据,避免 prop 逐级传递
- Event Bus - 非父子组件通信,小型项目简单场景
- Vuex/Pinia - 复杂应用状态管理,多组件共享状态
在实际开发中,应根据具体场景选择最合适的通信方式。
简单场景优先使用props/emit,
复杂状态管理考虑Pinia/Vuex,
跨层级通信使用provide/inject。
正确选择组件通信方式可以使代码更加清晰、可维护性更高,同时也能提高应用的性能。