13

Vue组件库开发:另类通信方式

 3 years ago
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自带的原生的emiton的观察者模式

此处demo可见官方文档

弊端:必须经过父组件,并且必须为此给父组件增加一个状态。如果组件层级过深,不可维护!

方案二:自己实现一个broadcastdispatch

虽然broadcastdispatch方法已经被Vue官方所废弃,但是我们仍然可以自己实现一个broadcastdispatch方法。原理是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);
        },

    }
};

之后,我们可以通过Vuemixins的方式把上述方法引入到组件实例中:

import Emitter from '../../mixins/emitter';

export default {
    // ...
    mixins: [Emitter],
    // ...
}

思考如图所示的组件模型:

Dicom-ViewDicom-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;
});

如上,比起第一种方法,我们可以看到它的优势:即可以不需要一级一级地传递组件状态,因为broadcastdispatch是递归地向上或向下传递状态。但同时我们也看到了劣势:必须手动通过mixins的方式引入我们的辅助函数,并且,事件传递只能单向。递归可能造成溢出的风险,性能损耗等等。

甚至还有这样的情况,Dicom-View是我们的公共组件,我们并不想在业务组件里面都引入这样的broadcastdispatch方法!并且,我们并不会把所有功能组件的内容全都暴露给业务组件来调用,个人认为,这样的方式缺乏可行性。

方案三:使用闭包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来进行功能组件的开发,不失为一种便捷的方法。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK