个人博客:haichenyi.com。感谢关注
一. 目录
二. 父子组件通信
- Props(父 → 子)
原理: 单向数据流,父组件通过属性传递数据,子组件申明接收
适用场景: 父组件向子组件传递静态或动态数据
<!-- 父组件 Parent.vue -->
<template>
<Child :user="userData" :isAdmin="false" />
</template>
<script>
import Child from './Child.vue';
export default {
components: { Child },
data() {
return {
userData: { name: 'Alice', age: 28 }
}
}
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>
<p>用户名:{{ user.name }}</p>
<p v-if="isAdmin">管理员权限</p>
</div>
</template>
<script>
export default {
props: {
user: {
type: Object,
required: true
},
isAdmin: {
type: Boolean,
default: false
}
}
}
</script>
简化一下就是:
<!-- 父组件 -->
<Child :message="parentMsg" />
<!-- 子组件 -->
<script>
export default {
props: {
message: {
type: String,
required: true
}
}
}
</script>
关键点:
- Props遵循单向数据流,子组件不能直接修改数据
- 对象类型传递的是引用,直接修改属性会触发父组件更新
那么,既然,子组件里面无法修改这个属性,那么,子组件里面想修改,该怎么办呢?只能想办法通过子组件触发父组件的方法,在父组件的方法里面去触发更新。$emit
- 自定义事件(子—>父)
原理: 子组件通过$emit触发事件,父组件监听事件并处理
适用场景: 表单提交、子组件状态变化通知
<!-- 父组件 Parent.vue -->
<template>
<Child @submit="handleSubmit" />
</template>
<script>
import Child from './Child.vue';
export default {
components: { Child },
methods: {
handleSubmit(formData) {
console.log('收到子组件数据:', formData);
// 发送到API...
}
}
}
</script>
<!-- 子组件 Child.vue -->
<template>
<form @submit.prevent="submitForm">
<input v-model="inputValue" />
<button type="submit">提交</button>
</form>
</template>
<script>
export default {
data() {
return { inputValue: '' }
},
methods: {
submitForm() {
this.$emit('submit', this.inputValue); // 触发父组件监听的事件
}
}
}
</script>
简化一下
<!-- 父组件 -->
<Child @submit="handleSubmit" />
<!-- 子组件 -->
<button @click="$emit('submit', data)">提交</button>
Vue3中,该用法有点变化,需要在子组件中申明事件,父组件写法不变
<!-- 子组件 ChildComponent.vue -->
<script>
export default {
emits: ['submit'], // 声明会触发 submit 事件(简单写法)
// 或带参数验证(推荐写法)
emits: {
submit: (payload) => {
// 验证 payload 是否符合预期
return typeof payload === 'object';
}
},
methods: {
handleClick() {
this.$emit('submit', { data: 'test' }); // 触发 submit 事件
}
}
}
</script>
所以,子组件里面如果想要修改父组件里面的值。就是需要通过$emit,触发父组件的事件。
那么,掌握了以上这两种方式,那你就掌握了 v-model 和 .sync 的双向绑定。
- v-model,sync 双向绑定(高级父子通信)
<!-- 父组件 -->
<ChildComponent v-model="message" />
<!-- 等价于 -->
<ChildComponent
:value="message"
@input="message = $event"
/>
<!-- 子组件 -->
<script>
export default {
props: ['value'],
methods: {
updateValue(newValue) {
this.$emit('input', newValue); // 触发 input 事件
}
}
}
</script>
v-model在vue2中只能绑定一个属性,vue3中能绑定多个属性。
vue2中如果想要双向绑定多个属性适用.sync。vue3中绑定多个属性适用v-model。废弃了.sync
<!-- 父组件 -->
<ChildComponent
:title.sync="pageTitle"
:content.sync="pageContent"
/>
<!-- 等价于 -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
<!-- 子组件 -->
<script>
export default {
props: ['title', 'content'],
methods: {
updateTitle(newTitle) {
this.$emit('update:title', newTitle); // 触发 title 更新
},
updateContent(newContent) {
this.$emit('update:content', newContent); // 触发 content 更新
}
}
}
</script>
简单来说v-model和.sync。就是系统帮我们按照规则定义好了传递数据的属性名和更新属性的方法名。他们的底层实现,还是prop和$emit
三. 兄弟组件通信
- 事件总线(Event Bus)
原理: 基于 Vue 实例的发布订阅模式,允许任意组件通信。
适用场景: 简单场景下的跨组件通知(如兄弟组件数据同步)。
// event-bus.js (Vue 2 示例)
import Vue from 'vue';
//一定要全局唯一,使用的是同一个实例
export const EventBus = new Vue();
<!-- 组件 A -->
<script>
import { EventBus } from './event-bus.js';
export default {
methods: {
notifyB() {
EventBus.$emit('message', '来自A组件的消息');
}
}
}
</script>
<!-- 组件 B -->
<script>
import { EventBus } from './event-bus.js';
export default {
created() {
EventBus.$on('message', (msg) => {
console.log('收到消息:', msg);
});
},
beforeDestroy() {
EventBus.$off('message'); // 避免内存泄漏
}
}
</script>
EventBus的原理,之前讲发布订阅模式的时候提到过。底层实现就是发布订阅模式(Pub/Sub)。
// Vue 2 的 EventBus 实现
const bus = new Vue();
bus.$on('event', callback); // 订阅
bus.$emit('event', data); // 发布
bus.$off('event'); // 取消订阅
至于具体的实现,可以看一下观察者模式与发布订阅模式:解耦与通信的艺术。了解一下发布订阅模式的原理,就大致知道它这个是怎么实现的了
- 全局状态管理(Vuex / Pinia)
原理: 集中式状态管理,通过 state 存储数据,mutations/actions 修改数据。
适用场景: 中大型项目,需要共享状态(如用户登录态、购物车)。
Vuex:基于响应式系统的状态管理,核心机制:
- 响应式状态(Reactivity): Vuex 的 state 通过 Vue 的响应式系统实现,当状态变化时,依赖该状态的组件自动更新。
// 内部通过 Vue 的 data 实现响应式
this._vm = new Vue({
data: { state: this.state }
});
- 同步更新(Mutations): 通过提交 mutation 修改状态,确保变更可追踪(同步)。
mutations: {
SET_USER(state, user) {
state.user = user; // 直接修改 state(同步)
}
}
- 异步处理(Actions): actions 处理异步逻辑(如 API 请求),最终通过提交 mutation 修改状态。
- 模块化(Modules): 支持将 Store 拆分为多个模块,解决大型项目状态管理问题。
// store/index.js (Vue 2)
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null
},
mutations: {
SET_USER(state, user) {
state.user = user;
}
},
actions: {
login({ commit }, userData) {
// 模拟API请求
setTimeout(() => {
commit('SET_USER', userData);
}, 1000);
}
}
});
<!-- 登录组件 Login.vue -->
<script>
export default {
methods: {
handleLogin() {
this.$store.dispatch('login', { name: 'Bob' });
}
}
}
</script>
<!-- 导航栏组件 Navbar.vue -->
<template>
<div v-if="$store.state.user">
欢迎, {{ $store.state.user.name }}
</div>
</template>
Pinia(Vue3 推荐):
- 更简洁的 API,支持 Composition API。
- 无 mutations,直接通过 actions 修改状态。
EventBus和Vuex的区别
特性 | EventBus | Vuex |
---|---|---|
设计目标 | 组件间通信 | 全局状态管理 |
数据存储 | 无状态 | 集中式响应式状态树 |
数据变更追踪 | 无 | 通过 mutations 记录变更历史 |
响应式更新 | 需手动触发 | 自动响应式更新 |
适用场景 | 简单通信(如兄弟组件通知) | 中大型项目全局状态管理 |
四. 跨层级组件通信
provide / inject
原理: 祖先组件提供数据,后代组件注入数据,跨越中间层级。
适用场景: 主题切换、多语言等全局配置。
Vue 2 的实现
<!-- 祖先组件 Ancestor.vue -->
<script>
export default {
provide: {
theme: 'dark' // 提供静态数据
},
// 或动态数据(需用函数形式)
provide() {
return {
theme: this.theme // 非响应式,祖先的 theme 变化不会传递
}
},
data() {
return { theme: 'dark' }
}
}
</script>
<!-- 后代组件 Descendant.vue -->
<script>
export default {
inject: ['theme'], // 注入数据
created() {
console.log(this.theme); // 输出 'dark'
}
}
</script>
Vue 3 的实现
<!-- 祖先组件 Ancestor.vue -->
<script setup>
import { provide, ref } from 'vue';
const theme = ref('dark'); // 响应式数据
provide('theme', theme); // 提供响应式引用
</script>
<!-- 后代组件 Descendant.vue -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme'); // 注入响应式数据
console.log(theme.value); // 输出 'dark'
// 修改祖先数据(需祖先提供更新方法)
const updateTheme = (newTheme) => {
theme.value = newTheme;
};
</script>
五. 直接访问组件实例(慎用)
使用this.$refs获取到组件实例
<!-- 父组件 -->
<Child ref="childRef" />
<script>
export default {
mounted() {
this.$refs.childRef.method(); // 调用子组件方法
}
}
</script>
缺点:
- 增加组件耦合性,不利于维护。
- Vue 3 已移除 $children,推荐使用 ref。
六. 通信方式对比与选择指南
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
Props / 事件 | 父子组件简单通信 | 简单直观 | 跨层级传递繁琐 |
v-model | 表单控件双向绑定 | 语法糖简化代码 | 仅限单个属性(Vue2) |
Event Bus | 简单兄弟组件通信 | 轻量灵活 | 难以调试,易内存泄漏 |
Vuex / Pinia | 复杂全局状态管理 | 数据流清晰,支持调试 | 增加项目复杂度 |
provide/inject | 跨层级数据传递(如主题) | 避免 Prop Drilling | 数据来源不透明 |
七. 总结
Vue 的组件通信方式多样,开发者应根据场景选择最合适的方案:
- 父子组件: 优先使用 props + 自定义事件。
- 兄弟组件: 简单场景用 Event Bus,复杂场景用 Vuex/Pinia。
- 跨层级组件: provide/inject 或全局状态管理。
- 慎用方案: parent/children 和直接修改 Props。
理解每种方式的原理和适用边界,才能写出高可维护性的 Vue 代码。