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 相关文章推荐
JS 学习笔记 防止发生命名冲突
Jul 30 Javascript
window.event快达到全浏览器支持了,以后使用就方便了
Nov 30 Javascript
使用Plupload实现直接上传附件至七牛云存储
Dec 26 Javascript
Angular 中 select指令用法详解
Sep 29 Javascript
jQuery实现ajax无刷新分页页码控件
Feb 28 Javascript
详解vue-router 2.0 常用基础知识点之router.push()
May 10 Javascript
Angular4.0中引入laydate.js日期插件的方法教程
Dec 25 Javascript
微信小程序实现折叠与展开文章功能
Jun 12 Javascript
vue.js 实现点击展开收起动画效果
Jul 07 Javascript
node.js中Buffer缓冲器的原理与使用方法分析
Nov 23 Javascript
通过实例了解Javascript柯里化流程
Mar 03 Javascript
vue项目中企业微信使用js-sdk时config和agentConfig配置方式详解
Dec 15 Vue.js
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
150kHz到30Mhz完全冲浪手册
2020/03/20 无线电
PHP 获取客户端真实IP地址多种方法小结
2010/05/15 PHP
Joomla简单判断用户是否登录的方法
2016/05/04 PHP
Thinkphp集成抖音SDK的实现方法
2020/04/28 PHP
基于jQuery的实现简单的分页控件
2010/10/10 Javascript
工作需要写的一个js拖拽组件
2011/07/28 Javascript
jQuery Ajax使用FormData对象上传文件的方法
2016/09/07 Javascript
利用JS判断字符串是否含有数字与特殊字符的方法小结
2016/11/25 Javascript
bootstrap suggest搜索建议插件使用详解
2017/03/25 Javascript
JS Input里添加小图标的两种方法
2017/11/11 Javascript
微信小程序获取手机网络状态的方法【附源码下载】
2017/12/08 Javascript
vue中锚点的三种方法
2018/07/06 Javascript
Layui实现数据表格默认全部显示(不要分页)
2019/10/26 Javascript
在Echarts图中给坐标轴加一个标识线markLine
2020/07/20 Javascript
[04:52]第二届DOTA2亚洲邀请赛主赛事第一天比赛集锦:OG娜迦海妖放大配合谜团大中3人
2017/04/02 DOTA
git使用.gitignore设置不生效或不起作用问题的解决方法
2017/06/01 Python
Python OpenCV获取视频的方法
2018/02/28 Python
python通过TimedRotatingFileHandler按时间切割日志
2019/07/17 Python
解决python3 安装不了PIL的问题
2019/08/16 Python
Django自定义模板过滤器和标签的实现方法
2019/08/21 Python
使用Python获取爱奇艺电视剧弹幕数据的示例代码
2021/01/12 Python
CSS3中伪元素::before和::after的用法示例
2017/09/18 HTML / CSS
用css3实现转换过渡和动画效果
2020/03/13 HTML / CSS
全球最大的中文旅行网站:去哪儿网
2017/11/16 全球购物
Java平台和其他软件平台有什么不同
2015/06/05 面试题
大学系主任推荐信范文
2013/12/24 职场文书
黄继光的英雄事迹材料
2014/02/13 职场文书
社团活动总结
2014/04/28 职场文书
终止或解除劳动合同及劳动关系的证明书
2014/10/06 职场文书
工作检讨书大全
2015/01/26 职场文书
小升初自荐信怎么写
2015/03/26 职场文书
2015年平安创建工作总结
2015/04/29 职场文书
2015年小学校长工作总结
2015/05/19 职场文书
2016年社区中秋节活动总结
2016/04/05 职场文书
竞聘开场白方式有哪些?
2019/08/28 职场文书
MongoDB日志切割的三种方式总结
2021/09/15 MongoDB