在前两篇文章中,我们学习了Props、ref、自定义事件和provide/inject等组件通信方式。今天我们将继续深入学习四种更高级的通信手段:全局事件总线 、消息订阅与发布 、插槽技术 以及组件实例的其他属性。掌握这些内容,你将能应对几乎所有组件通信场景。
一、全局事件总线(Global Event Bus)
1.1 什么是全局事件总线?
全局事件总线的核心思想是:给项目中所有的组件找一个共享的vc对象,所有的事件都绑定到这个共享对象上,所有组件都通过这个共享对象来传递数据。
共享对象必须具备两个特征:
- 能够让所有的vc共享访问
- 拥有
$on、$off、$emit等方法
1.2 实现原理
方式一:显式创建Vue组件实例
// main.js
import Vue from 'vue';
import App from './App.vue';
// 创建一个VueComponent实例作为总线
const VueComponent = Vue.extend({});
const globalVc = new VueComponent();
Vue.prototype.$bus = globalVc;
new Vue({
render: h => h(App)
}).$mount('#app');
方式二:在根实例beforeCreate中挂载(推荐)
// main.js
import Vue from 'vue';
import App from './App.vue';
new Vue({
beforeCreate() {
// 将当前vm挂载到Vue原型上,所有组件都能访问
Vue.prototype.$bus = this;
},
render: h => h(App)
}).$mount('#app');
推荐使用方式二,因为:
- 代码更简洁
- 直接利用已有的vm实例,无需额外创建
- 避免了不必要的性能开销
1.3 事件通信模式
发送事件(触发方)
girl.vue - 发送数据:
<template>
<div class="girl">
<h2>girl组件</h2>
<h3>girlNum: {{ girlNum }}</h3>
<button @click="sendGirlNum">点击传递数据给son</button>
</div>
</template>
<script>
export default {
name: 'girl',
data() {
return {
girlNum: 777
};
},
methods: {
sendGirlNum() {
// 通过$bus触发事件,传递数据
this.$bus.$emit('getGirlNum', this.girlNum);
}
}
};
</script>
child.vue - 发送数据:
<template>
<div class="child">
<h3>child组件</h3>
<button @click="$bus.$emit('sendMsg', childMsg)">点击传递数据给son</button>
</div>
</template>
<script>
export default {
name: 'child',
data() {
return {
childMsg: '我是child组件的数据'
};
}
};
</script>
接收事件(接收方)
son.vue - 接收数据:
<template>
<div class="son">
<h2>son组件</h2>
<h3>来自girl组件的girlNum: {{ girlNum }}</h3>
<h3>来自child组件的消息: {{ childMsg }}</h3>
</div>
</template>
<script>
export default {
name: 'son',
data() {
return {
girlNum: '',
childMsg: ''
};
},
mounted() {
// 在son组件中绑定事件,接收数据
this.$bus.$on('getGirlNum', (val) => {
this.girlNum = val;
});
this.$bus.$on('sendMsg', (val) => {
this.childMsg = val;
console.log('接收child数据', val);
});
},
beforeDestroy() {
// 组件销毁前清理事件监听
this.$bus.$off();
}
};
</script>
1.4 核心原则
永远记住:A组件向B组件传数据,应该在B组件中绑定事件(接收),在A组件中触发事件(发送)。
| 角色 | 行为 | API |
|---|---|---|
| 发送方(A组件) | 触发事件 | this.$bus.$emit('eventName', data) |
| 接收方(B组件) | 绑定事件 | this.$bus.$on('eventName', callback) |
| 清理 | 解绑事件 | this.$bus.$off() |
1.5 注意事项
| 注意点 | 说明 |
|---|---|
| 事件清理 | 在 beforeDestroy 中务必解绑事件,防止内存泄漏 |
| 事件命名 | 避免事件名冲突,建议使用统一的命名规范 |
| 调试难度 | 事件总线使数据流向不清晰,大型项目建议使用Vuex |
二、消息订阅与发布(PubSub)
2.1 什么是PubSub?
PubSub是一种跨框架通用的消息机制,通过发布/订阅模式实现组件间的通信。它不仅限于Vue,在任何前端框架甚至原生JS中都可以使用。
安装:
npm install pubsub-js
2.2 配置全局PubSub
// main.js
import Vue from 'vue';
import App from './App.vue';
import pubsub from 'pubsub-js';
// 将pubsub挂载到Vue原型上
Vue.prototype.$pubsub = pubsub;
new Vue({
render: h => h(App)
}).$mount('#app');
2.3 发布消息
girl.vue - 发布消息:
<template>
<div class="girl">
<h2>girl组件</h2>
<h3>girlNum: {{ girlNum }}</h3>
<button @click="sendMsg">点击发布消息</button>
</div>
</template>
<script>
export default {
name: 'girl',
data() {
return {
girlNum: 888
};
},
methods: {
sendMsg() {
// 发布消息,传递数据
this.$pubsub.publish('girlNum', this.girlNum);
}
}
};
</script>
2.4 订阅消息
son.vue - 订阅消息:
<template>
<div class="son">
<h2>son组件</h2>
<h3>来自girl组件的girlNum: {{ girlNum }}</h3>
</div>
</template>
<script>
export default {
name: 'son',
data() {
return {
girlNum: '',
pid: '' // 存储订阅ID,用于取消订阅
};
},
mounted() {
// 订阅消息
this.pid = this.$pubsub.subscribe('girlNum', (name, val) => {
this.girlNum = val;
console.log('消息名:', name, '数据:', val);
});
},
beforeDestroy() {
// 组件销毁前,取消消息的订阅
this.$pubsub.unsubscribe(this.pid);
}
};
</script>
2.5 PubSub vs 事件总线
| 对比项 | 全局事件总线 | PubSub |
|---|---|---|
| 所属框架 | Vue特有 | 跨框架通用 |
| 实现方式 | 基于Vue实例的$on/$emit |
第三方库 |
| 安装 | 无需安装 | 需要安装pubsub-js |
| 回调参数 | 直接接收数据 | 第一个参数为消息名,第二个为数据 |
| 清理方式 | $off() |
unsubscribe() |
三、插槽技术(Slots)
插槽是Vue中内容分发的重要机制,允许父组件向子组件传递模板内容。
3.1 默认插槽
**父组件(App.vue)** - 传递内容:
<template>
<div id="app">
<Myltem title="名著">
<div>
<h4 v-for="item in books" :key="item">{{ item }}</h4>
</div>
</Myltem>
</div>
</template>
<script>
import Myltem from './components/Myltem.vue';
export default {
name: 'App',
data() {
return {
books: ['红楼梦', '西游记', '水浒传', '三国演义']
};
},
components: { Myltem }
};
</script>
**子组件(Myltem.vue)** - 接收内容:
<template>
<div class="my-item">
<h3>{{ title }}</h3>
<!-- 默认插槽,显示父组件传递的内容 -->
<slot>默认内容(当父组件没有传递内容时显示)</slot>
</div>
</template>
<script>
export default {
name: 'Myltem',
props: ['title']
};
</script>
3.2 具名插槽
当一个组件中有多个插槽时,需要使用具名插槽来区分。
**子组件(Myltem.vue)** - 定义具名插槽:
<template>
<div class="my-item">
<h3>{{ title }}</h3>
<slot name="books">默认书籍内容</slot>
<hr>
<slot name="more">更多内容</slot>
</div>
</template>
**父组件(App.vue)** - 三种写法:
<template>
<div id="app">
<Myltem title="名著">
<!-- 写法一:使用slot属性(Vue2.6以前的方式) -->
<a href="#" slot="more">查看更多</a>
<!-- 写法二:使用template + #简写(推荐) -->
<template #books>
<h4 v-for="item in books" :key="item">{{ item }}</h4>
</template>
<!-- 写法三:使用template + v-slot -->
<template v-slot:more>
<a href="#">查看更多</a>
</template>
</Myltem>
</div>
</template>
三种写法对比:
| 写法 | 语法 | 适用版本 | 推荐程度 |
|---|---|---|---|
| 写法一 | slot="name" |
Vue2.6+ | 兼容旧版 |
| 写法二 | #name |
Vue2.6+ | ⭐推荐 |
| 写法三 | v-slot:name |
Vue2.6+ | 完整语法 |
3.3 作用域插槽
作用域插槽允许子组件向父组件传递数据,父组件可以根据这些数据来决定如何渲染插槽内容。
**子组件(Myltem.vue)** - 传递数据给父组件:
<template>
<div class="my-item">
<h3>{{ title }}</h3>
<!-- 通过插槽属性传递数据给父组件 -->
<slot :books="books" :names="names" :movies="movies"></slot>
</div>
</template>
<script>
export default {
name: 'Myltem',
data() {
return {
books: ['红楼梦', '西游记', '水浒传', '三国演义'],
names: ['张三', '李四', '王五'],
movies: ['流浪地球', '战狼', '长津湖']
};
}
};
</script>
**父组件(App.vue)** - 接收子组件数据:
<template>
<div id="app">
<!-- 写法一:scope接收数据 -->
<Myltem title="名著">
<template scope="data">
<h4 v-for="item in data.books" :key="item">{{ item }}</h4>
</template>
</Myltem>
<!-- 写法二:解构赋值 -->
<Myltem title="人名">
<template scope="{ names }">
<ul>
<li v-for="item in names" :key="item">{{ item }}</li>
</ul>
</template>
</Myltem>
<!-- 写法三:slot-scope(Vue2.6之前的写法) -->
<Myltem title="电影">
<template slot-scope="{ books }">
<h4 v-for="(book, index) in books" :key="index">{{ book }}</h4>
</template>
</Myltem>
</div>
</template>
作用域插槽的三种写法:
| 写法 | 语法 | 说明 |
|---|---|---|
scope |
scope="data" |
Vue2.6+推荐 |
| 解构 | scope="{ books }" |
更简洁 |
slot-scope |
slot-scope="{ books }" |
旧版写法,兼容 |
3.4 插槽总结
| 插槽类型 | 用途 | 子组件定义 | 父组件使用 |
|---|---|---|---|
| 默认插槽 | 单一内容分发 | <slot> |
直接写内容 |
| 具名插槽 | 多内容分发 | <slot name="xxx"> |
#xxx 或 v-slot:xxx |
| 作用域插槽 | 子传数据给父组件渲染 | <slot :data="data"> |
scope 或 slot-scope |
四、组件实例的其他属性
4.1 $children - 访问子组件
$children 返回当前组件的所有直接子组件实例的数组。
// App.vue
export default {
methods: {
getAllChild() {
// 获取所有直接子组件
console.log(this.$children);
// 可以遍历访问每个子组件的数据和方法
this.$children.forEach(child => {
console.log(child.$options.name);
});
}
}
};
⚠️ 注意事项:
$children不保证顺序- 依赖于子组件的挂载顺序
- 不建议频繁使用,推荐使用
ref精确访问
4.2 parent和root - 访问父组件和根实例
// son.vue
export default {
methods: {
getParent() {
// 获取直接父组件
console.log(this.$parent);
// 获取根实例(vm)
console.log(this.$root);
// 通过$parent可以访问父组件的数据和方法
console.log(this.$parent.parentData);
this.$parent.parentMethod();
}
}
};
| 属性 | 说明 | 返回值 |
|---|---|---|
$parent |
当前组件的直接父组件 | VueComponent实例 |
$root |
根Vue实例(vm) | Vue实例 |
4.3 attrs和listeners - 属性透传
当需要在中间组件透传属性和事件时,$attrs 和 $listeners 非常有用。
**父组件(App.vue)** - 传递属性和事件:
<template>
<div id="app">
<son :appNum="appNum" :appMsg="appMsg" @appFun="appFun" />
</div>
</template>
<script>
export default {
data() {
return {
appNum: 123,
appMsg: '来自App的消息'
};
},
methods: {
appFun() {
console.log('触发了appFun事件');
}
}
};
</script>
**中间组件(son.vue)** - 透传属性和事件:
<template>
<div class="son">
<h2>son组件</h2>
<!-- 通过v-bind="$attrs"和v-on="$listeners"透传 -->
<child v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import child from './child.vue';
export default {
name: 'son',
components: { child }
};
</script>
**最终子组件(child.vue)** - 使用透传的属性:
<template>
<div class="child">
<h3>child组件</h3>
<!-- 使用从son透传过来的属性 -->
<h4>appNum: {{ $attrs.appNum }}</h4>
<h4>appMsg: {{ $attrs.appMsg }}</h4>
<!-- 触发透传过来的事件 -->
<button @click="$emit('appFun')">触发事件</button>
</div>
</template>
<script>
export default {
name: 'child'
};
</script>
attrs和listeners 的作用:
| 属性 | 说明 | 用途 |
|---|---|---|
$attrs |
父组件传递的非prop属性 | 透传属性到更深层组件 |
$listeners |
父组件传递的自定义事件 | 透传事件到更深层组件 |
五、核心概念总结
5.1 通信方式一览
| 通信方式 | 方向 | 适用场景 | 复杂度 |
|---|---|---|---|
| 全局事件总线 | 任意组件 | 中小型项目,任意组件通信 | ⭐⭐ |
| PubSub | 任意组件 | 跨框架项目,消息机制 | ⭐⭐ |
| 默认插槽 | 父→子 | 内容分发 | ⭐ |
| 具名插槽 | 父→子 | 多区域内容分发 | ⭐ |
| 作用域插槽 | 子→父 | 子组件控制渲染逻辑 | ⭐⭐ |
| parent/children | 父子 | 快速访问父子组件 | ⭐ |
| attrs/listeners | 跨级 | 属性/事件透传 | ⭐⭐⭐ |
5.2 最佳实践建议
| 场景 | 推荐方案 |
|---|---|
| 父子组件通信 | Props + 自定义事件 |
| 兄弟组件通信 | 全局事件总线 |
| 跨级组件通信 | provide/inject 或 Vuex |
| 内容分发 | 插槽(默认/具名/作用域) |
| 属性透传 | $attrs + $listeners |
| 跨框架通信 | PubSub |
5.3 注意事项清单
- ✅ 使用全局事件总线后,务必在
beforeDestroy中解绑事件 - ✅ PubSub需要在组件销毁前取消订阅
- ✅ 作用域插槽的子组件通过插槽属性传递数据,父组件通过
scope接收 - ✅
$children不保证顺序,推荐使用ref替代 - ✅
$attrs不包含被props声明的属性 - ✅ 大型项目推荐使用Vuex管理状态,而非事件总线
从基础的Props到高级的事件总线和插槽,你已经掌握了Vue2组件通信的完整知识体系。在实际开发中,根据具体场景选择合适的通信方式,能让你的代码更加清晰和高效。