vue内部初始化时会为每个组件实例挂载一个this._events私有的空对象属性:

vm._events = Object.create(null) // 没有__proto__属性

这个里面存放的就是当前实例上的自定义事件集合,也就是自定义事件中心,它存放着当前组件所有的自定义事件。和自定义事件相关的API分为以下四个:this.$onthis.$emitthis.$offthis.$once,它们会往这个事件中心中添加、触发、移除对应的自定义事件,从而组成了vue的自定义事件系统,接下来看下它们都是怎么实现的。 * ### this.$on

描述:监听当前实例上的自定义事件。事件可以由vm.$emit触发,回调函数会接收所有传入事件触发函数的额外参数。

export default {created() {this.$on('test', res => {console.log(res)    })},methods: {handleClick() {this.$emit('test', 'hello-vue~')}}
}

以上示例首先在created钩子内往当前组件实例的事件中心_events中添加一个名为test的自定义事件,第二个参数为该自定义事件的回调函数,而触发handleClick这个方法后,就会在事件中心中尝试找到test自定义事件,触发它并传递给回调函数hello-vue~这个字符串,从而打印出来。我们来看下$on的实现:

Vue.prototype.$on = function (event, fn) {const hookRE = /^hook:/    //检测自定义事件名是否是hook:开头const vm = thisif (Array.isArray(event)) {  // 如果第一个参数是数组for (let i = 0; i < event.length; i++) {this.$on(event[i], fn)  // 递归}} else {(vm._events[event] || (vm._events[event] = [])).push(fn)// 如果有对应事件名就push,没有创建为空数组然后pushif (hookRE.test(event)) {  // 如果是hook:开头vm._hasHookEvent = true  // 标志位为true}}return vm
}

以上就是$on的实现了,它接受两个参数,自定义事件名event和对应的回调函数fn。主要就是往事件中心_events下挂载对应的event事件名key,而事件名对应的key又是一个数组形式,这样相同事件名的回调会在一个数组之内。而接下来的_hasHookEvent标志位表示是否监听组件的钩子函数,这个之后示例说明。

  • this.$emit

描述:触发当前实例上的事件,附加参数都会传给监听器回调。

Vue.prototype.$emit = function (event) {const vm = thislet cbs = vm._events[event]  // 找到事件名对应的回调集合if (cbs) {const args = toArray(arguments, 1)  // 将附加参数转为数组for (let i = 0; i < cbs.length; i++) {cbs[i].apply(vm, args)  // 挨个执行对应的回调集合}}return vm
}

$emit的实现会更好理解些,首先从事件中心中找到event对应的回调集合,然后将$emit其余参数转为args数组,最后挨个执行回调集合内的回调并传入args。通过这么一对朴实的API可以帮我们理解三件小事:

1. 理解自定义事件原理

app.vue
<template><child-component @test='handleTest' />
</template>
export default {methods: {handleTest(res) {console.log(res)}}
}----------------------------------------child.vue
<template><button @click='onClick'>btn</button>
</template>
export default {methods: {onClick() {this.$emit('test', 'hello-vue~')}}
}

以上是父子组件通过自定义事件通信,想必大家非常熟悉。自定义事件的实现原理和通常解释的会不同,它们的原理是父组件在经过编译模板后,会将定义在子组件上的自定义事件test及其回调handleTest通过$on添加到子组件的事件中心中,当子组件通过$emit触发test自定义事件时,会在它的事件中心中去找test,找到后传递hello-vue~给回调函数并执行,不过因为回调函数handleTest是在父组件作用域内定义的,所以看起来就像是父子组件之间通信般。

2. 监听组件的钩子函数

也就是$on内自定义事件名之前是hook:的情况,可以监听组件的钩子函数触发:

app.vue
<template><child-component @hook:created='handleHookEvent' />
</template>

以上示例为当子组件的created钩子触发时,就触发父组件内定义的handleHookEvent回调。接下来让我们再看一个官网的示例,使用这个特性如何帮我们写出更优雅的代码:

监听组件钩子之前:
mounted () {this.picker = new Pikaday({  // Pikaday是一个日期选择库field: this.$refs.input,format: 'YYYY-MM-DD'})
},
beforeDestroy () {  // 销毁日期选择器this.picker.destroy()
}监听组件钩子之后:
mounted() {this.attachDatepicker('startDateInput')this.attachDatepicker('endDateInput')  // 同时为两个input添加日期选择
},
methods: {attachDatepicker(refName) {  // 封装为一个方法const picker = new Pikaday({  // Pikaday是一个日期选择库field: this.$refs[refName],  // 为input添加日期选择format: 'YYYY-MM-DD'})this.$once('hook:beforeDestroy', () => {  // 监听beforeDestroy钩子picker.destroy()  // 销毁日期选择器})  // $once和$on类似,只是只会触发一次}
}

首先不用在当前实例下挂载一个额外的属性,其次可以封装为一个方法,复用更方便。

3. 不借助vuex跨组件通信

再开发组件库时,因为都是独立的组件,从而引入vuex这种强依赖是不现实的,而且很多时候是用插槽来放置子组件,所以子组件的位置、嵌套、数量并不会确定,从而在组件库内完成跨组件的通信就尤为重要。

通过接下来的示例介绍组件库中会运用到的一种,使用$on$emit来实现跨组件通信,子组件通过父组件的name属性找到对应的实例,找到后使用$emit触发父组件的自定义事件,而在这之前父组件已经使用$on完成了自定义事件的添加:

export default {methods: {  // 混入mixin使用dispatch(componentName, eventName, params) {let parent = this.$parent || this.$root  // 找父组件let name = parent.$options.name  // 父组件的name属性while (parent && (!name || name !== componentName)) {  // 和传入的componentName进行匹配parent = parent.$parent  // 一直向上查找if (parent) {name = parent.$options.name  // 重新赋值name}}if (parent) {  // 找到匹配的组件实例parent.$emit.apply(parent, [eventName].concat(params))  // $emit触发自定义事件}}}
}

接下来介绍表单验证组件内的使用案例:

不知道大家是否对这种表单验证好奇过,为什么点一下提交,就可以将所有的表单项全部做验证,接下来笔者试着写一个极简的表单验证组件来说明它的原理。这里会有两个组件,一个是iForm为整个表单,一个是iFormItem为其中的某个表单项:

iForm组件:<template><div> <slot /> </div>  // 只有一个插槽
</template><script>
export default {name: "iForm",  // 组件名很重要data() {return {fields: []  // 收集所有表单项的集合};},created() {this.$on("on-form-item-add", field => {  // $on必须得比$emit先执行,因为要先添加嘛this.fields.push(field)  // 添加到集合内});},methods: {validataAll() {  // 验证所有的接口方法this.fields.forEach(item => {item.validateVal()  // 执行每个表单项内的validateVal方法});}}
};
</script>

模板只有一个slot插槽,这个组件主要是做两件事,将所有的表单项的实例收集到fields内,提供一个可以验证所有表单项的方法validataAll,然后看下iFormItem组件:

<template><div><input v-model="curValue" style="border: 1px solid #aaa;" /><span style="color: red;" v-show="showTip">输入不能为空</span></div>
</template><script>
import emitter from "./emitter"  // 引入之前的dispatch方法export default {name: "iFormItem",mixins: [emitter],  // 混入data() {return {curValue: "",  // 表单项的值showTip: false  // 是否验证通过};},created() {this.dispatch("iForm", "on-form-item-add", this)  // 将当前实例传给iForm组件},methods: {validateVal() {  // 某个表单项的验证方法if (this.curValue === "") {  // 不能为空this.showTip = true  // 验证不通过}}}
};
</script>

看到这里我们知道了原来这种表单验证原理是将每个表单项的实例传入给iForm,然后在iForm内遍历的执行每个表单项的验证方法,从而可以一次性验证完所有的表单项。表单验证调用方式:

<template><div><i-form ref='form'>  // 引用<i-form-item /><i-form-item /><i-form-item /><i-form-item /><i-form-item /></i-form><button @click="submit">提交</button></div>
</template><script>
import iForm from "./form"
import iFormItem from "./form-item"export default {methods: {submit() {this.$refs['form'].validataAll() // 验证所有}},components: {iForm, iFormItem}
};
</script>

这里就使用了$on$emit这么一对API,通过组件的名称去查找组件实例,不论嵌套以及数量,然后使用事件API去跨组件传递参数。

注意点:当$on$emit配合使用时,$on要优先与$emit执行。因为首先要往实例的事件中心去添加事件,才能被触发。

  • this.$off

描述:移除自定义事件监听器,不过根据传入的参数分为三种形式:

  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
export default {created() {this.$on('test1', this.test1)this.$on('test2', this.test2)},mounted() {this.$off()  // 没有参数,清空事件中心}
}-------------------------------------------export default {created() {this.$on('test1', this.test1)this.$on('test2', this.test2)},mounted() {this.$off('test1')  // 在事件中心中移除test1}
}-------------------------------------------export default {created() {this.$on('test1', this.test1)this.$on('test1', this.test3)this.$on('test2', this.test2)},mounted() {this.$off('test1', this.test3)  // 在事件中心中移除事件test1的test3回调}
}

知道了这个API的调用方式之后,接下来看下$off的实现方式:

Vue.prototype.$off = function (event, fn) {const vm = thisif (!arguments.length) {  // 如果没有传递参数vm._events = Object.create(null)  // 重置事件中心return vm}if (Array.isArray(event)) {  // event如果是数组for (let i = 0, l = event.length; i < l; i++) {vm.$off(event[i], fn)  // 递归清空}return vm}if (!fn) {  // 只传递了事件名没回调vm._events[event] = null  // 清空对应所有的回调return vm}const cbs = vm._events[event]  // 获取回调集合let cblet i = cbs.lengthwhile (i--) {cb = cbs[i]  // 回调集合里的每一项if (cb === fn || cb.fn === fn) {  // cb.fn为$once时挂载的cbs.splice(i, 1)  // 找到对应的回调,从集合内移除break}}return vm
}

也是分为了三种情况,根据参数的不同做分别处理。 * ### this.$once

描述:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。

效果和$on是类似的,只是说触发一次之后会从事件中心中移除。所以它的实现思路也很好理解,首先通过$on实现功能,当触发之后从事件中心中移除这个事件。来看下它的实现原理:

Vue.prototype.$once = function (event, fn) {const vm = thisfunction on () {vm.$off(event, on)fn.apply(vm, arguments)}on.fn = fn  // 回调挂载到on下,移除时好做判断vm.$on(event, on)  // 将on添加到事件中心中return vm
}

首先将回调fn挂载到on函数下,将on函数注册到事件中心去,触发自定义事件时首先会在$emit内执行on函数,在on函数内执行$offon函数移除,然后执行传入的fn回调。这个时候事件中心没有了on函数,回调函数也执行了一次,完成$once功能~

事件API总结:$on往事件中心添加事件;$emit是触发事件中心里的事件;$off是移除事件中心里的事件;$once是触发一次事件中心里的事件。哪怕是如此不显眼的API,再理解了它们的实现原理后,也能让我们再更多场景更好的使用它们~

最后按照惯例我们还是以一道vue可能会被问到的面试题作为本章的结束(想不到事件相关特别好的题目~)。

面试官微笑而又不失礼貌的问道:

  • 说下自定义事件的机制。

怼回去:

  • 子组件使用this.$emit触发事件时,会在当前实例的事件中心去查找对应的事件,然后执行它。不过这个事件回调是在父组件的作用域里定义的,所以$emit里的参数会传递给父组件的回调函数,从而完成父子组件通信。

顺手点个赞或关注呗,找起来也方便~

胡成:你可能会用的上的一个vue功能组件库,持续完善中...​zhuanlan.zhihu.com

axure日期选择器组件_Vue原理解析(十):搞懂事件API原理及在组件库中的妙用相关推荐

  1. vue中在当前组件中定义的全局变量怎么在methods中使用_Vue原理解析(十):搞懂事件API原理及在组件库中的妙用...

    在vue内部初始化时会为每个组件实例挂载一个this._events私有的空对象属性: vm._events = Object.create(null) // 没有__proto__属性 这个里面存放 ...

  2. 【Hadoop】HDFS操作、数据上传与下载原理解析、高级特性及底层原理

    HDFS操作.数据上传与下载原理解析.高级特性及底层原理 1 HDFS操作 1.1 Web Console网页工具 1.2 命令行 1.2.1 普通的操作命令 1.2.2 管理员命令 1.3 Java ...

  3. php service原理,轻松搞懂WebService工作原理

    用更简单的方式给大家谈谈WebService,让你更快更容易理解,希望对初学者有所帮助. WebService是基于网络的.分布式的模块化组件. 我们直接来看WebService的一个简易工作流程: ...

  4. Android 动态分区详解(一) 5 张图让你搞懂动态分区原理

    文章目录 0. 导读 1. 动态分区详解的背景 1.1 背景 1.2 动态分区的本质 2. Linux device mapper 驱动 3. Android 动态分区布局 3.1 动态分区布局 3. ...

  5. 网站工作原理,你搞懂了吗?

    网站工作原理,你搞懂了吗? 网站工作原理 输入网站地址,按下回车会发生啥? 1. DNS解析 2. TCP连接 2.1 TCP介绍 3. 发送HTTP请求 3.1 HTTPS协议 3.2 HTTPS过 ...

  6. axure日期选择器组件_vue干货分享,超过六种组件通信方法讲解和精髓归纳

    好消息:为了更好的规划和组织内容,今后每期内容之后能将预告下期的主题,欢迎大家补充 组件的分类 常规页面组件,由 vue-router 产生的每个页面,它本质上也是一个组件(.vue),主要承载当前页 ...

  7. axure日期选择器控件_JavaFX 控件 - 输入 (Control - Inputs)

    本章重点内容 介绍JavaFX常用输入控件,从 GitHub 或 Gitee下载详细demo代码. 按钮.单选框.复选框等 适用范围 根据官方文档 javafx.scene.control 编写,适合 ...

  8. 深入理解异步I/O+epoll+协程,附上epoll原理解析以及协程现实与原理剖析视频

    前言 同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行:而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会 ...

  9. reactrouter监听路由变化_一篇文章搞懂前端路由原理解析和实现方式

    在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂. 想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本原理 ...

最新文章

  1. 面对疫情,在家办公的程序员如何突围
  2. wget指定目录下载以及其它的使用方式
  3. 趁有空,再了解一下GROOVY中关于类的通例
  4. 北斗导航 | 卫星导航系统时间转换:闰年(附C源代码)
  5. 1128:图像模糊处理
  6. 程序开机全屏且不能见任何windows界面、不能使用系统热键
  7. C#使用NPOI进行word的读写
  8. LeetCode 1739. 放置盒子(数学)
  9. 能量采集(HYSBZ-2005)
  10. 众成翻译2.0上线,翻译即有机会获赠图书
  11. 实战MongoDB-Replication之Master-Slave
  12. cookie和session笔记
  13. event.srcElement 与event.target
  14. 4G手机网络通信是如何被黑客远程劫持的?
  15. 【openjudge 计算概论(A)】[函数递归练习(3)]
  16. 计算机课程用的ps是哪个版本,ps哪个版本适用于新手?
  17. 连接网络要求输入计算机密码是什么,上网时总是提示需要输入网络密码什么原因...
  18. Veritas NetBackup 7.7.3 + Vistor 虚拟带库
  19. 中粮、益海品牌集中度提高,中小米企机会在高端细分市场
  20. 探索 Android Q:位置权限

热门文章

  1. Springboot毕设项目车险承保系统y3iv8(java+VUE+Mybatis+Maven+Mysql)
  2. 企业管理考试复习精炼版
  3. k8s2-5日常使用操作指令
  4. kaggle住房预测项目——第1部分
  5. Tensorflow2.0:CNN 解决凯斯西储大学轴承数据集的分类问题
  6. Vue+iview将表格table以excel文件导出的几种方式
  7. 图论基础知识与常见图处理算法
  8. 批处理bat FFMpeg 视频裁切(画面一分多)
  9. 突然想分析下房贷利率及利息计算
  10. python实现按照文件名称进行文件分类