这篇文章讲一讲组件中的通信方式,组件之间交互是一个常见的场景。
1、props-父组件传递数据给子组件
这是常用的一种父子组件传递数据方法
父组件中引入子组件,传递数据后子组件的<template>标签会替换父组件中的子组件标签嵌入到父组件的页面中。
props支持的完整参数:
1)type(类型校验)
作用:指定该属性期望接收的数据类型。如果父组件传递了错误的数据类型,Vue 会在控制台抛出警告,帮助开发者尽早发现潜在问题。
支持的类型:String、Number、Boolean、Array、Object、Date、Function、Symbol。
多类型支持:可以使用数组形式指定多个可能的类型,例如 type: String, Number。
2)required(是否必传)
作用:定义该属性是否为必填项。默认值为 false。如果设置为 true 且父组件没有传递该属性,Vue 会发出警告提示。 示例:required: true
3)default(默认值)
**作用:**当父组件未传递该属性时,子组件将使用此默认值。
注意:
基础数据类型(如 String、Number、Boolean)可以直接赋值,例如 default: 'Hello' 或 default: 0。
引用数据类型(如 Array、Object)必须通过工厂函数返回。例如写成 default: () => \[\] 或 default: () => ({})。这是为了防止多个组件实例共享同一个引用对象而导致数据互相污染。
4)validator(自定义验证器)
作用:提供一个自定义的验证函数,用于检查传入的值是否符合特定的业务逻辑或格式要求。该函数会接收传入的值作为唯一参数,必须返回一个布尔值(true 表示验证通过,false 表示验证失败并触发警告)。
示例:
validator(value) {
// 限制传入的状态只能是这三种之一
return ['success', 'error', 'warning'].includes(value);
}
用一个父,子组件来示例如何传数据:
html
<!--子组件-->
<template>
<div class="child-box">
<h2>{{ title }}</h2>
<p>数量是:{{ count }}</p>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<span>当前状态:{{ status }}</span>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: {
title: {
type: String,
required: true // 标题是必填的字符串
},
count: {
type: Number,
default: 0 // 数量默认为 0
},
list: {
type: Array,
default: () => [] // 列表默认为空数组(使用工厂函数)
},
status: {
type: String,
validator(value) { // 自定义状态校验
return ['success', 'error', 'warning'].includes(value);
}
}
}
}
</script>
这里的子组件展示了type,requied,default,validator,接下来在父组件用<"子组件名"/>标签在里面传递数据。
html
<!--父组件-->
<template>
<div class="parent-box">
<!--
向子组件传递数据:
1. title: 必填项,直接传字符串即可
2. count: 数字类型,必须加冒号 `:`
因为要传的是数字,如果不加冒号,Vue 会把它当成纯文本字符串 "10",从而触发类型校验失败警告。
3. my-list: 数组类型,必须加冒号 `:`(HTML中驼峰转短横线)
4. status: 只能传 success / error / warning
-->
<ChildComponent
title="这是一篇测试文章"
:count="10"
:my-list="myList"
status="success"
/>
<!--
【演示默认值】这个组件故意不传 count、list 和 status
子组件会自动使用 default 里配置的默认值(0 和 [])
-->
<ChildComponent
title="另一篇文章(使用默认值)"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'; // 引入子组件
export default {
name: 'ParentComponent',
components: { ChildComponent }, // 注册子组件
data() {
return {
myList: ['标签A', '标签B', '标签C'] // 准备传给子组件的数组数据
}
}
}
</script>
这样设计父、子组件合作的好处(作用)
1)组件复用
在不同的父组件想用到子组件的时候可以一个子组件不停的复用,传递对应的数据即可。虽然多个父组件引用的是同一个 .vue 文件,但当它们被渲染到页面上时,Vue 会为每个父组件生成完全独立的子组件实例。
2)动态传数据
在实际开发中,绝大多数数据都是异步获取的(比如调用后端 API)。 当页面刚加载时,数据可能还不存在。父组件发起网络请求,等几秒钟后数据拿到了,立刻通过 Props 传给子组件,子组件瞬间渲染出来。
3)解耦与可维护
父子传参保证了数据的源头统一由父组件控制,子组件只负责"给什么数据就展示什么内容"。修改起来更安全方便。
2、$emit子传父:
props可以让父组件传递参数给子组件,这里可以用$ emit实现子组件向父组件传递数据或者触发状态变更。
核心子组件:商品卡片
子组件在函数中通过$emit发送,父组件通过@接受,如果有n个同样类型的子组件实例,他们都执行一次函数的话父组件会接受n次信号。
这个组件只负责展示传入的数据,不包含任何获取数据的逻辑
html
<template>
<div class="goods-card">
<h3>{{ goods.name }}</h3>
<p>价格:¥{{ goods.price }}</p>
<!-- 点击按钮触发内部方法 -->
<button @click="handleAddCart">加入购物车</button>
</div>
</template>
<script>
export default {
// 1. 使用 props 接收父组件传来的数据
props: ['goods'],
methods: {
handleAddCart() {
// 2. 通过 this.$emit 向父组件发射名为 'add-cart' 的事件,并带上数据
this.$emit('add-cart', this.goods);
}
}
};
</script>
父组件 A:首页
这里的v-for会渲染多个子组件出来,每个子组件都包含物品的名字价格和加入购物车函数。
html
<template>
<div class="home-page">
<h1>首页</h1>
<GoodsCard
v-for="item in homeGoodsList"
:key="item.id"
:goods="item"
@add-cart="handleAddToCart"
/>
</div>
</template>
<script>
import GoodsCard from './GoodsCard.vue';
export default {
// 1. 注册子组件
components: {
GoodsCard
},
// 2. data 必须写成函数的形式
data() {
return {
homeGoodsList: [
{ id: 1, name: 'iPhone 15', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 12999 }
]
};
},
methods: {
handleAddToCart(goods) {
console.log(`在【首页】把 ${goods.name} 加入了购物车`);
}
}
};
</script>
父组件 B:搜索结果页
搜索结果页也引入了完全相同的子组件,但传递的是另一套完全不同的数据。
html
<template>
<div class="search-page">
<h1>搜索结果 - "手机"</h1>
<GoodsCard
v-for="item in searchGoodsList"
:key="item.id"
:goods="item"
@add-cart="handleAddToCart"
/>
</div>
</template>
<script>
import GoodsCard from './GoodsCard.vue';
export default {
components: { GoodsCard },
data() {
return {
searchGoodsList: [
{ id: 101, name: '小米 14', price: 3999 },
{ id: 102, name: '华为 Mate 60', price: 6999 }
]
};
},
methods: {
handleAddToCart(goods) {
console.log(`在【搜索页】把 ${goods.name} 加入了购物车`);
}
}
};
</script>
3、parent/children(不推荐)
1. $parent:子组件直接访问父组件
- 作用 :子组件可以通过
this.$parent直接获取父组件的实例对象,从而读取或修改父组件的数据和调用其方法。 - 示例 :如果父组件有一个数据
houseCount,子组件可以直接通过this.$parent.houseCount--来减少房产数量。
2. $children:父组件批量访问子组件
- 作用 :父组件可以通过
this.$children获取一个包含所有直接子组件实例的数组。 - 示例 :假设一个父组件下有多个表单子组件,父组件可以通过遍历
this.$children,批量检查每个子组件的表单是否填写完整(例如this.$children.every(child => child.isFilled))。
3、缺点:
- 破坏了单向数据流 :Vue 的核心设计哲学是"数据永远单向向下流动,事件永远向上冒泡"。使用
$parent直接修改父组件数据,会导致数据流向混乱,一旦项目变大,代码将变得极难调试和维护。 - 增加了组件耦合度 :子组件如果直接依赖
this.$parent,就意味着这个子组件"死绑"在了这个特定的父组件上,失去了独立性和复用性。
4、refs
核心机制:
在子组件标签上添加 ref 属性,然后在父组件的 JS 逻辑中通过 this.$refs.名称 来获取该子组件的完整实例。
直接访问子组件属性和调用子组件方法
例如这里在标签加上ref后可以直接访问子组件的属性和调用子组件的方法。
html
<template>
<div class="parent-box">
<!-- 1. 给子组件添加 ref 属性 -->
<ChildComponent ref="myChild" />
<!-- 2. 添加几个按钮来触发操作 -->
<button @click="callIncrease">让子组件计数+1</button>
<button @click="callReset">重置子组件</button>
<button @click="readData">读取子组件数据</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
methods: {
// 调用子组件的 increaseCount 方法
callIncrease() {
// 通过 this.$refs.myChild 获取实例并调用方法
this.$refs.myChild.increaseCount()
},
// 调用子组件的 reset 方法
callReset() {
this.$refs.myChild.reset()
},
// 直接读取子组件的数据,并调用返回值方法
readData() {
// 直接访问子组件的 data 属性
console.log('子组件的标题是:', this.$refs.myChild.title)
// 调用子组件的方法并接收返回值
const status = this.$refs.myChild.getStatus()
console.log('子组件当前状态:', status)
}
}
}
</script>
5、EventBus-任意组件的通信
原理:
创建一个空的 Vue 实例作为中央事件中心。发送方调用这个空的实例 emit 触发事件,接收方用 on 监听事件。
第一步:创建EventBus
首先,我们需要创建一个专门用来发送和接收事件的公共实例。通常我们会单独建一个文件(如 eventBus.js),利用空的 Vue 实例来实现:
javascript
// utils/eventBus.js
import Vue from 'vue';
const eventBus = new Vue(); // 创建一个空的 Vue 实例作为事件中心
export default eventBus;
第二步:发送方组件
在需要发送数据的组件中,引入事件总线,并在合适的时机(如点击按钮时)通过 $emit 触发一个自定义事件,并把数据传递出去:
html
<!-- ComponentA.vue -->
<template>
<button @click="sendMsg">发送消息给组件B</button>
</template>
<script>
import eventBus from '@/utils/eventBus';
export default {
methods: {
sendMsg() {
// 触发名为 'transfer-data' 的事件,并携带数据
eventBus.$emit('transfer-data', { msg: 'Hello, 我是组件A!' });
}
}
}
</script>
第三步:接受方组件
在需要接收数据的组件中,引入同一个事件总线,并在组件挂载时(mounted 生命周期)使用 $on 监听刚才定义的事件,此时就能得到发送方发过来的数据。
html
<!-- ComponentB.vue -->
<template>
<p>收到的消息:{{ receivedMsg }}</p>
</template>
<script>
import eventBus from '@/utils/eventBus';
export default {
data() {
return {
receivedMsg: ''
}
},
mounted() {
// 监听 'transfer-data' 事件
eventBus.$on('transfer-data', (data) => {
this.receivedMsg = data.msg;
});
}
beforeDestroy() {
// 取消对 'transfer-data' 事件的监听,防止内存泄漏
eventBus.$off('transfer-data');
}
}
</script>
第四步:销毁监听
因为全局事件总线是独立于组件存在的,如果不取消监听,当组件 B 再次被创建时,就会重复绑定监听器,导致一次触发执行多次回调,甚至引发内存泄漏。所以当组件 B 被销毁时,必须手动取消对事件的监听。
html
beforeDestroy() {
// 取消对 'transfer-data' 事件的监听,防止内存泄漏
eventBus.$off('transfer-data');
}
6、Vuex-全局状态管理
多个毫无关系的组件都需要频繁读取和修改同一份数据,用 EventBus 会导致数据流向混乱,难以追踪。这时候可以使用Vuex。
原理:
Vuex相当于一个全局的"数据仓库"。所有组件都可以从这个仓库中读取数据,并且通过严格规定的规则来修改数据,保证了状态变更的可追踪性。
第一步:创建"数据仓库"(Store)
在项目中(通常是 src/store/index.js),我们创建一个集中管理状态的地方。Vuex 的核心由五大件组成,最常用的是前四个:
javascript
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
// 1. 在 Vue 2 中,必须将 Vuex 注册为 Vue 的插件
Vue.use(Vuex);
// 2. 使用 new Vuex.Store() 创建并导出 store 实例
export default new Vuex.Store({
// 1. State(状态):存放共享数据
state: {
count: 0
},
// 2. Mutations(同步修改):唯一能直接修改 State 的地方,必须是同步函数
mutations: {
ADD_COUNT(state, payload) {
state.count += payload;
}
},
// 3. Actions(异步操作):处理异步逻辑,内部通过 commit 调用 mutation
actions: {
asyncAddCount(context, payload) {
setTimeout(() => {
context.commit('ADD_COUNT', payload);
}, 1000);
}
},
// 4. Getters(计算属性):对 State 进行二次加工
getters: {
doubleCount(state) {
return state.count * 2;
}
}
});
第二步:在入口文件挂载仓库
javascript
// main.js
import Vue from 'vue';
import App from './App.vue';
import store from './store';
import router from './router'
// Vue 2 中关闭生产环境下的提示,减小打包体积
Vue.config.productionTip = false;
// 创建 Vue 根实例,并传入 store 和 App 组件
new Vue({
store, // 注入 Vuex store,使所有子组件可以通过 this.$store 访问
router,
render: h => h(App) // 渲染 App.vue 作为根组件
}).$mount('#app'); // 挂载到 id 为 app 的 DOM 节点
第三步:在任意组件中"使用"仓库
javascript
this.$store.dispatch('asyncAddCount', 10);
假设我们在 ComponentA 中触发修改,在 ComponentB 中展示结果。
dispatch 用来触发 Actions(处理异步):
javascript
this.$store.dispatch('asyncAddCount', 10);
commit 用来触发 Mutations(处理同步):
javascript
this.$store.commit('ADD_COUNT', 10);
组件 A(触发修改):
javascript
<template>
<button @click="handleAdd">点击异步增加</button>
</template>
<script>
export default {
methods: {
handleAdd() {
// 通过 dispatch 触发 Actions(处理异步)
this.$store.dispatch('asyncAddCount', 10);
}
}
}
</script>
组件 B(读取数据):
html
<template>
<div>
<!-- 直接读取 State -->
<p>当前计数:{{ $store.state.count }}</p>
<!-- 读取 Getters 派生出的数据 -->
<p>双倍计数:{{ $store.getters.doubleCount }}</p>
</div>
</template>