对Element-UI通讯机制有感

572 阅读2分钟

前言

在最近的面试中曾经两次被问到了Element-UI的通讯机制。老实说,我确实没有了解过Element-UI的源码,而且在第一次被问到时也不以为意。直到第二次被问到我才意识到,这难道也是一个热门考点?所以今天专门来看看Element-UI究竟用了什么特别的通讯机制。

Element-UI的通讯

上源码:

function broadcast(componentName, eventName, params) {
	//递归子组件,查找命名空间内组件
	this.$children.forEach(child => {
		var name = child.$options.componentName;
		
		if (name === componentName) {
			//分发子组件内订阅消息
			child.$emit.apply(child, [eventName].concat(params));
		} else {
			broadcast.apply(child, [componentName, eventName].concat([params]));
		}
	});
}

function 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));
	}
},

好处

我们可以看到Element-UI专门封装了broadcast/dispatch2个方法,通过向上或者向下来触发组件事件。这样的设计当然有好处:

  1. 可以隔代子组件调用方法。在一般的父传子通讯中,信息必须一代一代往下传。如A-B-C,如果想要从A向C通讯,就必须要B的配合。而broadcast方法可以实现对B的零侵入。
  2. 不难发现这样的实现其实本质上就是发布订阅,而发布订阅中最容易遇到的问题就是对命名空间的管理。而这种方法直接用组件名作为名命空间,在开发上可以降低对名命空间的管理压力。

缺陷

当然这个方案也有解决不了的问题:

  1. 无法同时触发子代和孙代的事件。可以看到2个方法都是执行了最近的一代组件的事件之后就停止了。如果想要在触发子代事件之后,也触发孙代的事件,就必须修改方法的停止边际。但这样又会导致一个新的问题,就是会让事件的触发链变长,出现无效遍历。
  2. 无法触发兄弟组件的事件。无论是broadcast还是dispatch,都只能向上/下触发事件,并不能触发兄弟间的事件。

VUE中也有broadcast/dispatch?

然而broadcast/dispatch并不是Element-UI的原创。早在VUE 1.x版本中已经有了broadcast/dispatch,只是在2.x之后就被弃用了。让我们来看看当时VUE的实现

Vue.prototype.$dispatch = function (event) {
  var shouldPropagate = this.$emit.apply(this, arguments)
  if (!shouldPropagate) return
  var parent = this.$parent
  var args = toArray(arguments)
  args[0] = { name: event, source: this }
  while (parent) {
    shouldPropagate = parent.$emit.apply(parent, args)
    parent = shouldPropagate ? parent.$parent : null
  }  
  return this
}

Vue.prototype.$broadcast = function (event) {
  var isSource = typeof event === 'string'
  event = isSource ? event : event.name
  if (!this._eventsCount[event]) return
  var children = this.$children
  var args = toArray(arguments)
  if (isSource) {
    args[0] = { name: event, source: this }
  }
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i]
    var shouldPropagate = child.$emit.apply(child, args)
    if (shouldPropagate) {
      child.$broadcast.apply(child, args)
    }
  }
  return this
}

观察代码可以发现,VUE的broadcast/dispatch设计思路跟Element-UI的还是很不一样的 。VUE中的这2个方法,目的是向上/下派发某个事件,而且会在第一次接收到后停止冒泡,除非返回 true。

两者差异

其实严格来说,这两者之间是无法对比的。因为他们的设计目的并不一样,Element-UI中是向上/下触发某个组件的某个事件。而VUE是顺着组件链向上/下派发某个方法。

VUE为什么要弃用broadcast/dispatch?

我好奇的是VUE为什么要弃用broadcast/dispatch?这在VUE 2.x的官网文档中可以找到答案

文档地址:cn.vuejs.org/v2/guide/mi…

显然在1.x的时候,VUE的跨代通讯手段并不完整。诸如VUEX,EVENT BUGS这种方法还没有推广。当2.x之后官网更希望开发者可以用更加合理的手段。

我们是否应该用broadcast/dispatch?

既然Element-UI都在用,那我们在业务中是否应该也可以考虑用broadcast/dispatch呢?我个人认为在大多数的情况下broadcast/dispatch都不是我们最好的选择。

不得不承认在Element-UI作为一个组件库,使用broadcast/dispatch是一个不错的选择,因为组件库的设计往往是规范的,开发者可以在组件之间制定很多的条件束缚来减少边界情况。

而在业务项目中需求是不固定的,我们的代码会被经常修改,这意味着组件间严密的规范不太可能会被严格遵循。使用broadcast/dispatch很可能会出现命名空间混乱,额外组件事件被触发,代码冗余等问题。这时候VUEX,EVENT BUS显然是更好的选择。

总结

Element-UI的broadcast/dispatch是一个在某些场景下的有效设计,如果你正好在开发组件库,或者一些紧密的通用组件,broadcast/dispatc可能可以为你提供一种思路。但对于业务开发来说broadcast/dispatc很多时候并不是最好的方案。