概述
在vue中,我们常见的组件通信方式有如下几种:
- 父传子
- props,ref
- 子传父
- on
- props(父组件传递函数过来也行)
- 非父子组件通信
- vuex
- 自定义的发布订阅实现
- 跨多个层级的父子组件传值
- provide和inject(使用较少,不常用)
比较常见的几种传值通信方式主要为上面几种,基本上满足我们项目需求,从上面几种通信方式中,来重点看看on,在了解它的实现原理之前,我们先看看设计模式中的发布订阅
发布订阅
官方术语
- 在软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。
个人理解
发布订阅模式可以类比到日常生活中,比如销售房子,我们需要买一套房子,到售楼处去看了,当时不满意,看了又看,订阅了售楼处的新房通知,等到有新房可售的时候,对方(发布者)会通知我们(订阅者)。
类比前端实际开发
在dom操作中,必不可少的会给dom元素绑定事件处理函数,比如给一个按钮绑定了一个事件处理函数,等到我们点击页面按钮的时候,就会触发绑定的dom元素的事件触发。
类别到vue中
emit就相当于发布者发布事件,触发处理函数,我们经常给组件上面绑定自定义事件处理函数,然后通过$emit的方式触发,其实就是发布订阅模式的实现。
on
本质是,在组件上面的自定义事件其实就是下面这种写法的另一种形式。
mounted() {
//订阅事件
this.$on("onChange", () => {
console.log("测试出发了");
});
},
methods: {
handleTestClick() {
//这里就会触发上面mounted里面的订阅的回调函数
this.$emit("onChange");
},
}
自己实现一个发布订阅
在了解了发布订阅之后,其实实现起来还是蛮简单的,我们需要一个发布者,订阅者,以及保存这些回调函数的一个事件队列。
class Pubsub {
constructor() {
//保存事件类型和对应回调函数的队列
this.queenList = [];
}
// 订阅事件
subscribe(eventName, fn) {
let cur = this.queenList.find((item) => item.eventName == eventName);
if (cur) {
cur.callback.push(fn);
} else {
this.queenList.push({
eventName,
callback: [fn],
});
}
}
// 发布事件
notice(eventName, ...args) {
let cur = this.queenList.find((item) => item.eventName == eventName);
if (cur) {
cur.callback.forEach((fn) => {
fn(...args);
});
}
}
// 清空事件队列
clearEventQueen() {
this.queenList = [];
}
}
//使用
let pubsub=new Pubsub()
//订阅事件
pubsub.subscribe("onChange",(val)=>{
console.log("我订阅了一个事件,传递过来的参数是"+val)
})
//发布事件
pubsub.notice("onChange",2)
vue中无限层级父子组件通信优雅解决方案
在上面都掌握之后,我们来看看我们使用组件库的时候,会发现的一个问题,拿elementui中的select下拉组件来说,先看具体用法.
<el-select v-model="value" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
思考一个问题
有没有发现,select组件并不是一个组件实现的,而是通过select组件嵌套option组件,而且基本上所有组件库的思路都是这样,按照我们业务开发组件,肯定直接一个select组件,然后在select内部去使用option进行v-for,这样子避免组件之前的传值问题不好解决。但是组件库却不是这样的,其好处就是我们可以更好的扩展,我们视乎使用组件的时候,多了更多的选择,我们自己业务组件,也不给我别人用,当然不需要考虑这么多,因此我们可能会想,这样子的话,组件之间的通信岂不是很麻烦了吗?
解决方案
其实这就涉及到了跨多个层级之间的父子组件通信,我们可以使用发布订阅来解决。以下是处理方案。
/**
* @Description 由于涉及到跨组件之间通信,因此我们只有自己实现发布订阅的模式,来实现组件之间通信,灵感主要来源于element-ui组件库源码中跨层级父子组件通信方案,本质上也是发布订阅和$emit和$on
* @param { String } componentName 组件名
* @param { String } eventName 事件名
* @param { argument } params 参数
**/
// 广播通知事件
function _broadcast(componentName, eventName, params) {
// 遍历当前组件的子组件
this.$children.forEach(function (child) {
// 取出componentName,组件options上面可以自己配置
var name = child.$options.componentName;
// 如果找到了需要通知的组件名,触发组件上面的$eimit方法,触发自定义事件
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
// 没找到,递归往下找
_broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
const emiiter = {
methods: {
// 派发事件(通知父组件)
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
// 循环往上层父组件,知道知道组件名和需要触发的组件名相同即可,然后触发对应组件的事件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
// 广播事件(通知子组件)
broadcast(componentName, eventName, params) {
_broadcast.call(this, componentName, eventName, params);
},
},
};
export default emiiter;
总结
发布订阅在前端中,是非常重要且常用的一种设计模式,框架的实现都离不开它,因此我们需要深刻理解,上面实现了vue中的跨不同层级的父子组件通信的问题,下一期我会通过手写组件库select组件的方式,带你掌握这种方案的用法。