Vue组件库开发:另类通信方式
source link: https://zhuanlan.zhihu.com/p/35958092
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Vue组件库开发:另类通信方式
问题:不用Vuex
怎么让兄弟组件便捷通信?甚至让业务组件和内部组件通信?
答案:使用eventHub
如果不使用EventHub
,我们想让父组件的两个子组件,甚至两个孙子组件之间进行通信,怎么办?
方案一:Vue
自带的原生的emit
和on
的观察者模式
此处demo
可见官方文档
弊端:必须经过父组件,并且必须为此给父组件增加一个状态。如果组件层级过深,不可维护!
方案二:自己实现一个broadcast
和dispatch
虽然broadcast
和dispatch
方法已经被Vue
官方所废弃,但是我们仍然可以自己实现一个broadcast
和dispatch
方法。原理是componentName
参数传递需要被通知的组件,然后在组件树中用递归的方式找到正确的组件名称,之后通过apply
调用对应组件的$emit
方法:
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
function broadcastAll( eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
child.$emit.apply(child, [eventName].concat(params));
broadcastAll.apply(child, [eventName].concat([params]));
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
},
broadcastAll( eventName, params) {
broadcastAll.call(this, eventName, params);
},
}
};
之后,我们可以通过Vue
的mixins
的方式把上述方法引入到组件实例中:
import Emitter from '../../mixins/emitter';
export default {
// ...
mixins: [Emitter],
// ...
}
思考如图所示的组件模型:
Dicom-View
是Dicom-Canvas
的父级组件,Dicom-Canvas
组件又包括有多个Dicom-View-Port
组件,我们在Dicom-View-Port
中触发一个事件,希望改变另一个Dicom-View-Port
组件的状态。
我们在Dicom-View-Port
组件中,触发一个点击事件,此时想父组件发送一个名为on-click-select-view-port
的事件:
handleMouseDown(event) {
this.dispatch('DicomCanvas', 'on-click-select-view-port',[this.index, this.element, this.seriesId, this.windowName]);
},
在二级组件Dicom-Canvas
中,我们监听了这样的一个事件,希望向下广播,并希望Dicom-View
也接收这一事件,我们又在Dicom-Canvas
中向上传播这一事件:
// 监听被选中的视窗
this.$on('on-click-select-view-port',(index, element, seriesId, windowName)=>{
this.dispatch('DicomView','on-click-select-view-port',[index, element, seriesId, windowName]);
this.broadcast('DicomViewPort','on-click-select-view-port',[index]);
this.selectedViewPortIndex = index;
});
在Dicom-View
组件,监听到该事件,改变了状态:
// 监听被选中的视窗
this.$on('on-click-select-view-port', (index, element, seriesId, windowName) => {
this.selectedViewPortIndex = index;
this.element = element;
this.seriesId = seriesId;
this.windowName = windowName;
});
如上,比起第一种方法,我们可以看到它的优势:即可以不需要一级一级地传递组件状态,因为
broadcast
和dispatch
是递归地向上或向下传递状态。但同时我们也看到了劣势:必须手动通过mixins
的方式引入我们的辅助函数,并且,事件传递只能单向。递归可能造成溢出的风险,性能损耗等等。
甚至还有这样的情况,Dicom-View
是我们的公共组件,我们并不想在业务组件里面都引入这样的broadcast
和dispatch
方法!并且,我们并不会把所有功能组件的内容全都暴露给业务组件来调用,个人认为,这样的方式缺乏可行性。
方案三:使用闭包mixins
利用闭包不会被垃圾回收机制回收的特征,采用闭包minins
。参考Vue 另类状态管理
终极方案:使用eventHub
开发功能组件的问题是:如何定义功能组件供给外部组件调用的接口,常用的方式是,Vue
中我们一般是通过props
传递状态进入功能组件,在功能组件中watch
这个状态的变化。
看如图所示的情况:
我们在业务组件1
中引入我们的公共组件Dicom-View
,我们希望在业务组件2
中去监听Dicom-View-Port
组件的一个事件。
如果按照方案二的方式:由于Dicom-View
是功能组件暴露给外部的唯一接口,因此外部调用功能组件只能通过Dicom-View
的接口来进行调用,我们的业务组件2
必须通过整个应用的状态流转来流转到业务组件1
的调用处来进行调用,在不使用Vuex
的情况下,我们怎么避免如此冗余的调用链,那么eventHub
的模式就登场了:
eventHub
类似于服务定位器模式和观察者模式的结合,eventHub
为一个中心点,所有事件的监听和发送都会经过eventHub
这样一个中心点,如图所示:
EventHub
作为事件的中心定位器,所有的事件都经过eventHub
来进行转发,不需要经过父子组件中的状态传递,我们可以把EventHub
放在Vue
的原型下面,这样可以在组件实例中直接运用:
首先,在webpack
的入口处,定义EventHub
和所有事件的原型EVENTS
:
import Vue from 'vue';
import DicomView from './components/dicom-view';
import EVENTS from './utils/events';
Vue.prototype.$DicomView = DicomView;
Vue.prototype.$DicomView.$EventHub = new Vue();
Vue.prototype.$DicomView.$EVENTS = EVENTS;
在我们的Dicom-View-Port
组件中,注册事件:
handleMouseDown(event) {
this.$DicomView.$EventHub.$emit(EVENTS.ON_CLICK_SELECT_VIEW_PORT, {
index: this.index,
element: this.element,
seriesId: this.seriesId,
windowName: this.windowName,
});
// this.dispatch('DicomCanvas', 'on-click-select-view-port',[this.index, this.element, this.seriesId, this.windowName]);
},
在实际业务组件中,在created
钩子函数中,注册事件监听器:
created() {
this.$DicomView.$EventHub.$on(this.$DicomView.$EVENTS.ON_CLICK_SELECT_VIEW_PORT, ({ seriesId, index }) => {
this.activeSeriesId = seriesId;
});
}
最好在组件销毁前,使用$off
清除事件监听:
beforeDestroy() {
this.$DicomView.$EventHub.$off(this.$DicomView.$EVENTS.ON_CLICK_SELECT_VIEW_PORT, ({ seriesId, index }) => {
this.activeSeriesId = seriesId;
});
}
如上,就完成了上述的如此复杂的组件间消息通信。
总结:通过EventHub
的方式,更便捷清晰地解决了在不使用Vuex
的情况下的组件间通信和状态共享问题,更便捷地实现功能组件和业务组件的通信,使用EventHub
来进行功能组件的开发,不失为一种便捷的方法。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK