xingbofeng.github.io
xingbofeng.github.io copied to clipboard
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
来进行功能组件的开发,不失为一种便捷的方法。
赞,虽然我还没怎么研究 Vue ,但看面你上面的解析,我大概明白了 Vue 里数据的传递。且我理解你的 eventHub
方案应该也是 Vuex 的简化版,只是 Vuex 做的更强大、更完善些。
最近我也要进军 Vue 了,得多向你讨教了~
@xuexb 使用eventHub
的目的是为了解决特定的组件库开发过程中,组件层级较深,并且无法使用Vuex
情况下的状态通信问题。通常的组件库都是UI
组件库,往往层级较浅,仅仅依靠props down
和event up
就可以实现组件库与外部业务组件的状态通信。但此次组件库开发遇到的问题是组件层级较深的问题,所以探讨了几种通信形式。由于Vue
本身的状态绑定和数据通信就是通过Object.defineProperty
或者Proxy
代理的形式来拦截底层set
操作实现的观察者模式,就之前的EventHub
就直接使用了Vue
实例:
Vue.prototype.$DicomView.$EventHub = new Vue();
最近考虑到event
的触发顺序问题,因此自己实现了一个eventHub
,可见自己实现一个带权重的事件监听器