$dispatch 和 $broadcast 作为一对情侣 💑属性,在 Vue 1.0 中主要用来实现基于组件树结构的事件流通信 —— 通过向上或向下以冒泡的形式传递事件流,以实现嵌套父子组件的通信。但是由于其显功能缺陷,在 Vue 2.0 中就被移除了。虽然 Vue 官网已经不再支持使用 $dispatch 和 $broadcast 进行组件通信,但是在很多基于 Vue 的 UI 框架中都有对其的封装,包括 、iview 等等。
那么 $dispatch 和 $broadcast 到底是怎么工作,其底层又是怎么实现的呢?接下来,我们就详细的说一说!
01 $dispatch 详解
为了追根溯源,我们还是先去 Vue 1.0 的文档你观摩一下其概念吧!
概念:
Dispatch an event, first triggering it on the instance itself, and then propagates upward along the parent chain. The propagation stops when it triggers a parent event listener, unless that listener returns true. Any additional arguments will be passed into the listener's callback function.
上面的一段英文定义来自 Vue 1.0 官方文档,其大致的意思是说:dispatch 是一个事件,首先会在自己实例本身上触发,然后沿父链向上传播。当它触发父组件上的事件侦听器时传播即会停止,除非该侦听器返回 true。 任何其他参数都将传递给侦听器的回调函数。
参数:
dispatch 会接收两中参数:event 是事件名称,[...args] 是触发事件时传递给回调函数的参数。
**例子:
// 创建一个 parent 组件 var parent = new Vue(); // 创建一个 child1 组件,其父组件指向 parent var child1 = new Vue({ parent: parent }); // 创建一个 child2 组件,其父组件指向 child1 var child2 = new Vue({ parent: child1 }); // 在 parent 组件监听名为 test 的事件,并绑定了一个回调函数 parent.$on('test', function () { console.log('parent notified'); }); // 在 child1 组件监听名为 test 的事件,并绑定了一个回调函数 child1.$on('test', function () { console.log('child1 notified'); }); // 在 child2 组件监听名为 test 的事件,并绑定了一个回调函数 child2.$on('test', function () { console.log('child2 notified'); });
说到这里,parent、child1 和 child2 三个组件之间的关系可以展示成如下的关系图:
// 在 child2 组件中通过 dispatch 触发 test 事件 child2.$dispatch('test'); // 事件执行会输出如下结果 // -> "child2 notified" // -> "child1 notified"
当执行 child2.$dispatch('test'); 时,首先会触发 child2 组件里面监听的 test 事件的回调函数,输出 'child2 notified',根据上面官方文档的定义,事件会沿着组件关系链一直向上传递,然后传递到 child1 组件,触发监听事件输出 "child1 notified",但是该侦听器没有返回 true,所以事件传递到此就结束了,最终的输出结果就只有 "child2 notified" 和 "child1 notified"。
Vue 1.0 官方实现
在 Vue 1.0 版本中,$dispatch 实现的源码放在 /src/instance/api/events.js 文件中,代码很简单:
/** * Recursively propagate an event up the parent chain. * 递归地在父链上传播事件。 * @param {String} event * @param {...*} additional arguments */ // $dispatch 方法是定义在 Vue 的 prototype 上的 // 接受一个字符串类型的事件名称 Vue.prototype.$dispatch = function (event) { // 首先执行 $emit 触发事件,将返回值保存在 shouldPropagate 中 var shouldPropagate = this.$emit.apply(this, arguments) // 如果首次执行的 $emit 方法返回的值不是 true 就直接返回 // 如果返回值不是 true 就说明组件逻辑不希望事件继续往父组件进行传递 if (!shouldPropagate) return // 如果首次执行 $emit 方法返回值是 true 就获取当前组件的 parent 组件实例 var parent = this.$parent // 将函数接受的参数转换成数组 var args = toArray(arguments) // use object event to indicate non-source emit on parents // 根据传入的事件名称的参数组装成 object args[0] = { name: event, source: this } // 循环知道组件的父组件 while (parent) { // 在父组件中执行 $emit 触发事件 shouldPropagate = parent.$emit.apply(parent, args) // 如果父组件 $emit 返回的是 true 就继续递归祖父组件,否则就停止循环 parent = shouldPropagate ? parent.$parent : null } // 最后返回当前组件实例 return this }
element-ui 实现
在 element-ui 中,$dispatch 实现的源码放在 /src/mixins/emitter.js 文件中,代码很简单:
// 定义 dispatch 方法,接受三个参数,分别是:组件名称、将要触发的事件名称、回调函数传递的参数 dispatch(componentName, eventName, params) { // 获取基于当前组件的父组件实例,这里对父组件实例和根组件实例做了兼容处理 var parent = this.$parent || this.$root; // 通过父组件的 $option 属性获取组件的名称 var name = parent.$options.componentName; // 当相对当前组件的父组件实例存在,而且当父组件的名称不存在或者父组件的名称不等于传入的组件名称时,执行循环 while (parent && (!name || name !== componentName)) { // 记录父组件的父组件 parent = parent.$parent; // 当父组件的父组件存在时,获取祖父组件的名称 if (parent) { name = parent.$options.componentName; } } // 当循环结束是,parent 的值就是最终匹配的组件实例 if (parent) { // 当 parent 值存在时调用 $emit 方法 // 传入 parent 实例、事件名称与 params 参数组成的数组 // 触发传入事件名称 eventName 同名的事件 parent.$emit.apply(parent, [eventName].concat(params)); } }
差异分析
仔细看完实现 $dispatch 方式的两个版本的代码,大家是不是发现,两个版本的实现和功能差异性还是很大的。