Vue中的情侣属性$dispatch和$broadcast详解


Posted in Javascript onMarch 07, 2019

00 前言

$dispatch 和 $broadcast 作为一对情侣 ?属性,在 Vue 1.0 中主要用来实现基于组件树结构的事件流通信 —— 通过向上或向下以冒泡的形式传递事件流,以实现嵌套父子组件的通信。但是由于其显功能缺陷,在 Vue 2.0 中就被移除了。虽然 Vue 官网已经不再支持使用 $dispatch 和 $broadcast 进行组件通信,但是在很多基于 Vue 的 UI 框架中都有对其的封装,包括 element-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 三个组件之间的关系可以展示成如下的关系图:

Vue中的情侣属性$dispatch和$broadcast详解

// 在 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 方式的两个版本的代码,大家是不是发现,两个版本的实现和功能差异性还是很大的。

1、接受参数:Vue 实现版本只会接受一个字符串类型的事件名称为参数,而 element-ui 实现的版本会接受三个参数,分别是:需要触发事件的组件名称、将要触发的事件名称、回调函数传递的参数;

2、实现功能:Vue 实现版本触发事件一直会顺着组件链向上进行传递,知道父组件中的侦听器没有返回 true,在这个期间所有的组件都会执行事件的响应,包括当前组件本身,而 element-ui 实现版本会不断的基于当前组件向父组件进行遍历,直至找到和接受的组件名称匹配,就会停止遍历,触发匹配组件中的监听事件。

10 $broadcast 详解

上面详细的说完 $dispatch 方法的实现和 Vue 实现版本与 element-ui 实现版本的区别,下面就该说说 $broadcast,毕竟他们是情侣属性嘛。

概念

Broadcast an event that propagates downward to all descendants of the current instance. Since the descendants expand into multiple sub-trees, the event propagation will follow many different “paths”. The propagation for each path will stop when a listener callback is fired along that path, unless the callback returns true.

broadcast 是一个事件,它向下传播到当前实例的所有后代。由于后代扩展为多个子树,事件传播将会遵循许多不同的“路径”。 除非回调返回 true,否则在沿该路径触发侦听器回调时,每个路径的传播将会停止。

参数

broadcast 会接收两中参数:event 是事件名称,[...args] 是触发事件时传递给回调函数的参数。

例子

// 创建 parent 组件实例
var parent = new Vue()

// 创建 child1 组件实例,其父组件指向 parent
var child1 = new Vue({ parent: parent })

// 创建 child2 组件实例,其父组件指向 parent
var child2 = new Vue({ parent: parent })

// 创建 child3 组件实例,其父组件指向 child2
var child3 = new Vue({ parent: child2 })

// 在 child1 组件监听名为 test 的事件,并绑定了一个回调函数
child1.$on('test', function () {
 console.log('child1 notified')
})

// 在 child2 组件监听名为 test 的事件,并绑定了一个回调函数
child2.$on('test', function () {
 console.log('child2 notified')
})

// 在 child3 组件监听名为 test 的事件,并绑定了一个回调函数
child3.$on('test', function () {
 console.log('child3 notified')
})

parent、child1、child2 和 child3 四个组件之间的关系可以展示成如下的关系图:

Vue中的情侣属性$dispatch和$broadcast详解

parent.$broadcast('test')
// -> "child1 notified"
// -> "child2 notified"

当执行 parent.$broadcast('test'); 时,事件流会以 parent 组件为起点向 parent 的子组件进行传递,根据事件绑定的顺序,虽然 parent 组件有两个同级的 child1 和 child2 ,但是事件流会先触发 child1 里面的绑定事件,此时会输出 "child1 notified",然后事件流到达 child2 组件,会触发 child2 组件中的绑定事件,输出 "child2 notified"。到这时,child2 组件中的侦听器并没有返回 true,所以事件传递到此就结束了,最终的输出结果就只有 "child1 notified" 和 "child2 notified"。

Vue 1.0 官方实现

在 Vue 1.0 版本中,$broadcast 实现的源码放在 /src/instance/api/events.js 文件中,代码很简单:

/**
 * Recursively broadcast an event to all children instances.
 * 递归地向所有子实例广播事件。
 * @param {String|Object} event
 * @param {...*} additional arguments
 */
// $dispatch 方法是定义在 Vue 的 prototype 上的
// 接受一个事件
Vue.prototype.$broadcast = function (event) {
 // 获取传入事件的类型,判断是否为字符串
 var isSource = typeof event === 'string'
 
 // 校正 event 的值,当接受 event 的类型为字符串时就直接使用,如果不是字符串就使用 event 上的 name 属性 
 event = isSource ? event : event.name
 
 // if no child has registered for this event,
 // then there's no need to broadcast.
 // 如果当前组件的子组件没有注册该事件,就直接返回,并不用 broadcast
 if (!this._eventsCount[event]) return
 
 // 获取当前组件的子组件
 var children = this.$children
 
 // 将函数接受的参数转换成数组
 var args = toArray(arguments)
 
 // 如果传入事件为字符串
 if (isSource) {
  // use object event to indicate non-source emit
  // on children
  // 根据传入的事件名称的参数组装成 object
  args[0] = { name: event, source: this }
 }
 
 // 循环子组件
 for (var i = 0, l = children.length; i < l; i++) {
  var child = children[i]
  
  // 在每个子组件中调用 $emit 触发事件
  var shouldPropagate = child.$emit.apply(child, args)
  
  // 判断调用 $emit 返回的值是否为 true
  if (shouldPropagate) {
   // 如果调用 $emit 返回的值为 true,就递归孙子组件继续广播
   child.$broadcast.apply(child, args)
  }
 }
 
 // 最后返回当前组件的实例
 return this
}

element-ui 实现

在 element-ui 中,$broadcast 实现的源码放在 /src/mixins/emitter.js 文件中,代码很简单:

// 定义 broadcast 方法,接受三个参数,分别是:组件名称、将要触发的事件名称、回调函数传递的参数
function broadcast(componentName, eventName, params) {
 // 依次循环当前组件的子组件
 this.$children.forEach(child => {
  // 获取每个子组件的名字
  var name = child.$options.componentName;

  // 判断子组件的名字是否等于传入的组件名称
  if (name === componentName) {
   // 如果子组件的名字等于传入的组件名称就调用 $emit 触发事件
   child.$emit.apply(child, [eventName].concat(params));
  } else {
   // 如果子组件的名字不等于传入的组件名称就递归遍历调用 broadcast 孙子组件
   broadcast.apply(child, [componentName, eventName].concat([params]));
  }
 });
}

差异分析

和之前说到的 $dispatch 一样,这里的 $broadcast 的两个实现版本也存在着巨大的差异:

1、接受参数:Vue 实现版本只会接受一个字符串类型的事件名称为参数,而 element-ui 实现的版本会接受三个参数,分别是:需要触发事件的组件名称、将要触发的事件名称、回调函数传递的参数;

2、实现功能:Vue 实现的 $broadcast 触发方式是默认只触发子代组件,不触发孙子代组件,如果子代创建了监听且返回了true,才会向孙子代组件传递事件。而 element-ui 实现的版本是直接向所有子孙后代组件传递,直至获取到的子组件名称等于传入的组件名称相等,才会触发当前子组件的监听事件,期间也没有返回值的判定。

11 总结

说到这里,$dispatch 和 $broadcast 的讲解就结束了。可能大家已经知道了 Vue 2.0 版本为什么会将这两个属性移除。首先我们引入官网的说法:

因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。这种事件方式确实不太好,我们也不希望在以后让开发者们太痛苦。并且 $dispatch 和 $broadcast 也没有解决兄弟组件间的通信问题。

这样来说 $dispatch 和 $broadcast 确实会有这样的问题。在前面的讲解中,大家也不难发现 $dispatch 主要是事件流由当前组件往父组件流动,当满足一定条件的时候就会触发当前子组件的监听事件,$broadcast 的功能是事件流由当前组件向子组件流动,当满足一定条件的时候就会触发当前子组件的监听事件。也就是说 $dispatch 和 $broadcast 主要解决了父子组件、嵌套父子组件的通信,并没有解决兄弟组件的通信问题,另一个方面这样的事件流动的方式是基于组件树结构的,当业务越来越烦杂时,这种方式会显得极其繁琐,甚至会混乱到难以维护,所以 Vue 2.0 版本移除这两个 API 是在意料之中的。

但是为什么三方 UI 库都会封装类似的这样一个组件通信的方式呢?我的猜测可能是为了解决在父子层嵌套组件中,通过 $dispatch 和 $broadcast 定向的向某个父或者子组件远程调用事件,这样就避免了通过传 props 或者使用 refs 调用组件实例方法的操作。这样说的话,$dispatch 和 $broadcast 也就其存在的价值,而并不是一无是处的,还是那句话:技术没有好与坏,只有合适不合适!

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
JQuery的ajax基础上的超强GridView展示
Sep 18 Javascript
新增加的内容是如何将div的scrollbar自动移动最下面
Jan 02 Javascript
JavaScript限定图片显示大小的方法
Mar 11 Javascript
js实现上一页下一页的效果【附代码】
Mar 10 Javascript
js实现的简单图片浮动效果完整实例
May 10 Javascript
JQuery组件基于Bootstrap的DropDownList(完整版)
Jul 05 Javascript
javascript中的后退和刷新实现方法
Nov 10 Javascript
javascript实现将数字转成千分位的方法小结【5种方式】
Dec 11 Javascript
使用html+js+css 实现页面轮播图效果(实例讲解)
Sep 21 Javascript
jQuery第一次运行页面默认触发点击事件的实例
Jan 10 jQuery
vue中组件的过渡动画及实现代码
Nov 21 Javascript
JS左右无缝轮播功能完整实例
May 16 Javascript
JS/jQuery实现获取时间的方法及常用类完整示例
Mar 07 #jQuery
在Web关闭页面时发送Ajax请求的实现方法
Mar 07 #Javascript
mpvue微信小程序多列选择器用法之省份城市选择的实现
Mar 07 #Javascript
使用vue开发移动端管理后台的注意事项
Mar 07 #Javascript
vue插件mescroll.js实现移动端上拉加载和下拉刷新
Mar 07 #Javascript
从0到1构建vueSSR项目之node以及vue-cli3的配置
Mar 07 #Javascript
从0到1构建vueSSR项目之路由的构建
Mar 07 #Javascript
You might like
收音机发烧友应当熟知的100条知识
2021/03/02 无线电
连接到txt文本的超链接,不直接打开而是点击后下载的处理方法
2009/07/01 PHP
PHP 页面编码声明方法详解(header或meta)
2010/03/12 PHP
jQuery+PHP发布的内容进行无刷新分页(Fckeditor)
2015/10/22 PHP
PhpStorm配置Xdebug调试的方法步骤
2019/02/02 PHP
Smarty缓存机制实例详解【三种缓存方式】
2019/07/20 PHP
jquery checkbox 勾选的bug问题解决方案与分析
2014/11/13 Javascript
ie下js不执行的几种可能
2017/02/28 Javascript
深入讲解xhr(XMLHttpRequest)/jsonp请求之abort
2017/07/26 Javascript
微信小程序实现图片上传、删除和预览功能的方法
2017/12/18 Javascript
jquery.onoff实现简单的开关按钮功能(推荐)
2018/05/24 jQuery
vue v-for 使用问题整理小结
2019/08/04 Javascript
js实现移动端轮播图滑动切换
2020/12/21 Javascript
Python 遍历子文件和所有子文件夹的代码实例
2016/12/21 Python
python 根据pid杀死相应进程的方法
2017/01/16 Python
Python中对象的引用与复制代码示例
2017/12/04 Python
神经网络理论基础及Python实现详解
2017/12/15 Python
python中的闭包函数
2018/02/09 Python
使用Python和xlwt向Excel文件中写入中文的实例
2018/04/21 Python
Django重装mysql后启动报错:No module named ‘MySQLdb’的解决方法
2018/04/22 Python
浅谈Python接口对json串的处理方法
2018/12/19 Python
Django之无名分组和有名分组的实现
2019/04/16 Python
Python利用神经网络解决非线性回归问题实例详解
2019/07/19 Python
Python CVXOPT模块安装及使用解析
2019/08/01 Python
Python 线程池用法简单示例
2019/10/02 Python
解决pytorch-yolov3 train 报错的问题
2020/02/18 Python
python单元测试框架pytest的使用示例
2020/10/07 Python
详解CSS3伸缩布局盒模型Flex布局
2018/08/20 HTML / CSS
2014年人事科工作总结
2014/11/19 职场文书
校园安全学习心得体会
2016/01/18 职场文书
《圆的面积》教学反思
2016/02/19 职场文书
纯CSS实现hover图片pop-out弹出效果的实例代码
2021/04/16 HTML / CSS
Python time库的时间时钟处理
2021/05/02 Python
python实现简单聊天功能
2021/07/07 Python
如何打开Win11系统注册表编辑器?Win11注册表编辑器打开修复方法
2022/04/05 数码科技
纯CSS实现一个简单步骤条的示例代码
2022/07/15 HTML / CSS