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 相关文章推荐
javascript parseInt 函数分析(转)
Mar 21 Javascript
全面兼容的javascript时间格式化函数(比较实用)
May 14 Javascript
Egret引擎开发指南之创建项目
Sep 03 Javascript
易被忽视的js事件问题总结
May 14 Javascript
BootStrap中按钮点击后被禁用按钮的最佳实现方法
Sep 23 Javascript
vue.js实现表格合并示例代码
Nov 30 Javascript
Node.js与Sails redis组件的使用教程
Feb 14 Javascript
node.js中fs.stat与fs.fstat的区别详解
Jun 01 Javascript
深入理解React Native原生模块与JS模块通信的几种方式
Jul 24 Javascript
Node.js引入UIBootstrap的方法示例
May 11 Javascript
javascript简单实现深浅拷贝过程详解
Oct 08 Javascript
webpack打包html里面img后src为“[object Module]”问题
Dec 22 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
PHP 如何获取二维数组中某个key的集合
2014/06/03 PHP
php模拟post提交数据的方法
2015/02/12 PHP
php实现用已经过去多长时间的方式显示时间
2015/06/05 PHP
ThinkPHP实现生成和校验验证码功能
2017/04/28 PHP
Javascript模块化编程(一)AMD规范(规范使用模块)
2013/01/17 Javascript
js给dropdownlist添加选项的小例子
2013/03/04 Javascript
jQuery拖拽div实现思路
2014/02/19 Javascript
获取中文字符串的实际长度代码
2014/06/05 Javascript
推荐 21 款优秀的高性能 Node.js 开发框架
2014/08/18 Javascript
$(&quot;&quot;).click与onclick的区别示例介绍
2014/09/25 Javascript
jquery 操作css样式、位置、尺寸方法汇总
2014/11/28 Javascript
nodejs中的fiber(纤程)库详解
2015/03/24 NodeJs
nodejs redis 发布订阅机制封装实现方法及实例代码
2016/12/15 NodeJs
JS 实现计算器详解及实例代码(一)
2017/01/08 Javascript
JavaScript使用原型和原型链实现对象继承的方法详解
2017/04/05 Javascript
vue学习笔记之vue1.0和vue2.0的区别介绍
2017/05/17 Javascript
angular.js实现列表orderby排序的方法
2018/10/02 Javascript
vue中使用百度脑图kityminder-core二次开发的实现
2019/09/26 Javascript
nuxt 页面路由配置,主页轮播组件开发操作
2020/11/05 Javascript
微信小程序input抖动问题的修复方法
2021/03/03 Javascript
[39:18]完美世界DOTA2联赛PWL S3 Forest vs LBZS 第二场 12.17
2020/12/19 DOTA
Python编程实现从字典中提取子集的方法分析
2018/02/09 Python
浅谈numpy数组中冒号和负号的含义
2018/04/18 Python
解决django后台管理界面添加中文内容乱码问题
2019/11/15 Python
tensorflow 利用expand_dims和squeeze扩展和压缩tensor维度方式
2020/02/07 Python
详解python变量与数据类型
2020/08/25 Python
Python3中的tuple函数知识点讲解
2021/01/03 Python
挪威户外活动服装和装备购物网站:Bergfreunde挪威
2016/10/20 全球购物
香港永安旅游网:Wing On Travel
2017/04/10 全球购物
职业培训师职业生涯规划
2014/02/18 职场文书
读书演讲主持词
2014/03/18 职场文书
《陈涉世家》教学反思
2014/04/12 职场文书
机关门卫的岗位职责
2014/04/29 职场文书
优秀少先队辅导员先进事迹材料
2014/05/18 职场文书
测绘工程专业求职信
2014/07/15 职场文书
法院授权委托书范文
2014/08/02 职场文书