一、初始位置

平常项目中写逻辑,避免不了注册/触发各种事件

今天来研究下 Vue 中,我们平常用到的关于 on/emit/off/once 的实现原理

关于事件的方法,是在 Vue 项目下面文件中的 eventsMixin 注册的

src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index' function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
} initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue) // 此处初始化 Vue 关于事件相关的实例方法
lifecycleMixin(Vue)
renderMixin(Vue) export default Vue

二、源码解析

进入到 src/core/instance/events.js 文件中

这边提取了 on/emit/off/once 的相关代码,并做了注释

src/core/instance/events.js

/**
* @describtion 注册事件以及触发事件时要执行的函数
* @param event {string | Array<string>} 要注册的事件名,可以是个字符串,也可以是个数组,数组元素也是字符串
* @param fn {Function} 要注册的事件函数
* @return Component 返回 Vue 实例
*/
Vue.prototype.$on = function(event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 先判断传进来的 event 是否是个数组
if (Array.isArray(event)) {
// 是数组,则循环进行事件注册
// 多个事件名可以绑定同个函数
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// event 不是数组
// event 是个字符串
// 先判断 vm._events[event] 是否存在, 不存在则设置为空数组 []
// 这个 vm._events 在 new Vue 时候, Vue 里面 执行 this._init() 中
// 执行了 initEvents(vm)
// 在 initEvents(vm) 中
// export function initEvents(vm: Component) {
// vm._events = Object.create(null) // 这里创建了 _events 这个对象,用来存储事件
// vm._hasHookEvent = false
// // init parent attached events
// const listeners = vm.$options._parentListeners
// if (listeners) {
// updateComponentListeners(vm, listeners)
// }
// }
;(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
// 在注册的时候使用标记过的布尔值代替哈希查找,消费hook事件
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
} /**
* @describtion 和 $on 一样,注册事件以及触发事件时要执行的函数,但是只执行一次就销毁
* @param event {string} 要注册的事件名,是个字符串
* @param fn {Function} 要注册的事件函数
* @return Component 返回 Vue 实例
*/
Vue.prototype.$once = function(event: string, fn: Function): Component {
const vm: Component = this
// 将目标函数 fn 包装起来
// 注册时候使用包装的 on 函数注册
// 这样 on 函数被执行一次时,首先把自己从注册事件列表中销毁
// 然后执行实际的目标函数 fn // 如果是一开始就使用目标函数 fn 注册
// 然后在目标函数 fn 执行时候,销毁fn
// 做不到销毁自己的同时还能执行自己,所以需要把fn进行一次包装
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
// 因为对目标函数做了包装,此处是方便销毁事件时候做判断是否有事件要销毁以及要销毁的是哪个 fn
on.fn = fn
vm.$on(event, on)
return vm
} /**
* @describtion 销毁事件以及触发事件时要执行的函数
* @param event? {string | Array<string>} 可选。要销毁的事件名,可以是个字符串,也可以是个数组,数组元素也是字符串
* @param fn? {Function} 要销毁的事件函数 可选。
* @return Component 返回 Vue 实例
*/
Vue.prototype.$off = function(event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
// 如果没有参数,则将 vm_events 设置为空,表示销毁全部事件
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
// 如果 event 是个数组,则遍历 event,对每个事件进行销毁
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
// 上面两个是特殊情况,这里才是正常销毁逻辑
// 先通过传入的 event 字符串从 _events 对象中去取值
// 判断该 事件名底下是否有绑定的目标函数,没有则返回当前组件实例,啥也不做
const cbs = vm._events[event]
if (!cbs) {
return vm
}
// 或者没有传入之前注册时候的目标函数
// 那么就将 event 对应的所有目标函数都销毁
// vm._events[event] = null
if (!fn) {
vm._events[event] = null
return vm
} // specific handler
// 如果有传入 目标函数
// 对取出的 event 对应的 目标函数进行倒序遍历
// vm._events[event] 的值,经过前面的过滤,到这里一定是个数组
// 倒序遍历一个个数组元素,判断每一个元素与传入要销毁的目标函数是否相等
// 相等,则使用 splice 进行删除
// 删除数组的操作使用倒序处理,不至于在删除元素的时候,后续的元素序号向前进位,导致处理结果有误
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
} /**
* @describtion 触发事件
* @param event {string} 要触发的事件名,是个字符串
* @return Component 返回 Vue 实例
*/
Vue.prototype.$emit = function(event: string): Component {
const vm: Component = this
// 此处是开发环境代码,可以忽略
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(`Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".`)
}
}
// 通过传入的 event 从 _events 对象中获取目标函数
let cbs = vm._events[event]
if (cbs) {
// 如果有相应的目标函数
// Convert an Array-like object to a real Array.
// toArray 将一个类数组转换成真正的数组
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// 获取除了第一个事件名之外的其他参数
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
// 对得到的目标函数进行遍历,并传入相关参数
for (let i = 0, l = cbs.length; i < l; i++) {
// 该函数调用了当前目标函数,并处理目标函数的异常
// 比如 目标函数 返回一个 Promise 这里添加了 catch 处理
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}

三、实现例子

项目地址放在github上了,有需要的可以看下
Vue 的事件方法类实现例子

模式实现一个 Vue 的事件方法类

class VueEvent {
constructor() {
this._events = Object.create(null)
}
$on(event, fn) {
const vm = this
// 先判断传进来的 event 是否是个数组
if (Array.isArray(event)) {
// 是数组,则循环进行事件注册
// 多个事件名可以绑定同个函数
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 先判断 vm._events[event] 是否存在, 不存在则设置为空数组 []
;(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
$once(event, fn) {
const vm = this
// 将目标函数 fn 包装起来
// 注册时候使用包装的 on 函数注册
// 这样 on 函数被执行一次时,首先把自己从注册事件列表中销毁
// 然后执行实际的目标函数 fn // 如果是一开始就使用目标函数 fn 注册
// 然后在目标函数 fn 执行时候,销毁fn
// 做不到销毁自己的同时还能执行自己,所以需要把fn进行一次包装
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
// 因为对目标函数做了包装,此处是方便销毁事件时候做判断,是否有事件要销毁以及要销毁的是哪个 fn
on.fn = fn
vm.$on(event, on)
return vm
}
$off(event, fn) {
const vm = this // 如果没有参数,则将 vm_events 设置为空,表示销毁全部事件
if (!arguments.length) {
vm._events = Object.create(null)
return vm
} // 如果 event 是个数组,则遍历 event,对每个事件进行销毁
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
} // 上面两个是特殊情况,这里才是正常销毁逻辑
// 先通过传入的 event 字符串从 _events 对象中去取值
// 判断该 事件名底下是否有绑定的目标函数,没有则返回当前组件实例,啥也不做
const cbs = vm._events[event]
if (!cbs) {
return vm
} // 或者没有传入之前注册时候的目标函数
// 那么就将 event 对应的所有目标函数都销毁
// vm._events[event] = null
if (!fn) {
vm._events[event] = null
return vm
} // 如果有传入 目标函数
// 对取出的 event 对应的目标函数进行倒序遍历
// vm._events[event] 的值,经过前面的过滤,到这里一定是个数组
// 倒序遍历一个个数组元素,判断每一个元素与传入要销毁的目标函数是否相等
// 相等,则使用 splice 进行删除
// 删除数组的操作使用倒序处理,不至于在删除元素的时候,后续的元素序号向前进位,导致处理结果有误
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
$emit(event) {
const vm = this
// 通过传入的 event 从 _events 对象中获取目标函数
let cbs = vm._events[event]
if (cbs) {
// 如果有相应的目标函数
// 获取除了第一个事件名之外的其他参数
const args = Array.prototype.slice.call(arguments, 1)
// 对得到的目标函数进行遍历,并传入相关参数
for (let i = 0, l = cbs.length; i < l; i++) {
// 这里就不做 promise 的处理了,直接调用
cbs[i].apply(vm, args)
}
}
return vm
}
}

四、使用

let ev = new VueEvent()

// test $on
ev.$on('onEv', function(emitParam) {
console.log('test $on: ', emitParam)
console.log('onEv on')
console.log('\n************\n')
})
setTimeout(() => {
ev.$emit('onEv', 'emit 1')
}, 0)
setTimeout(() => {
ev.$emit('onEv', 'emit 2')
}, 1000)
// 输出
// test $on: emit 1
// onEv on // ************ // test $on: emit 2
// onEv on // test $once
ev.$once('onceEv', function(emitParam) {
console.log('test $once: ', emitParam)
console.log('onceEv on')
console.log('\n************\n')
})
setTimeout(() => {
ev.$emit('onceEv', 'emit 3')
}, 2000)
setTimeout(() => {
ev.$emit('onceEv', 'emit 4')
}, 3000)
// 输出
// test $once: emit 3
// onceEv on // test $off
ev.$on('offEv', function(emitParam) {
console.log('test $off: ', emitParam)
console.log('offEv on')
console.log('\n************\n')
})
setTimeout(() => {
ev.$emit('offEv', 'emit 5')
}, 4000)
setTimeout(() => {
ev.$emit('offEv', 'emit 6')
ev.$off('offEv')
}, 5000)
setTimeout(() => {
ev.$emit('offEv', 'emit 7')
}, 6000)
// 输出
// test $off: emit 5
// offEv on // ************ // test $off: emit 6
// offEv on

Vue 事件相关实例方法---on/emit/off/once的更多相关文章

  1. Vue 事件的高级使用方法

    Vue 事件的高级使用方法 事件方法 在Vue中提供了4中事件监听方法,分别是: $on(event: string | Array, fn) $emit(event: string) $once(e ...

  2. vue事件监听机制

    vue事件是同步的.如果绑定了事件(组件标签上绑定事件) 组件的事件触发 组件调用时绑定事件 之后监听事件: $emit 抛出后活等着 $on ,如果监听到了则阻塞执行: 如果为监听到或者未绑定,则会 ...

  3. Vue事件绑定原理

    Vue事件绑定原理 Vue中通过v-on或其语法糖@指令来给元素绑定事件并且提供了事件修饰符,基本流程是进行模板编译生成AST,生成render函数后并执行得到VNode,VNode生成真实DOM节点 ...

  4. Vue 中 $on $once $off $emit 详细分析,以及使用

    vue的 $on,$emit,$off,$once Api 中的解释: $on(eventName:string|Array, callback) 监听事件 监听当前实例上的自定义事件.事件可以由 v ...

  5. javascript 事件相关使用总结01

    javascript 事件相关使用总结01 这里总结一下js事件相关的经验. addEventLinstener()介绍 注册事件最基础的函数是这个 target.addEventListener(t ...

  6. UE4事件相关总结

    转自:http://blog.ch-wind.com/ue4-event-overview/ 事件机制是实现游戏内逻辑的重要部分,在开始进行游戏逻辑的设计和实现之前,对UE4的事件机制进行理解是非常必 ...

  7. Vue—事件修饰符

    Vue事件修饰符 Vue.js 为 v-on 提供了事件修饰符来处理 DOM 事件细节,如:event.preventDefault() 或 event.stopPropagation(). Vue. ...

  8. vue 事件上加阻止冒泡 阻止默认事件

    重点 vue事件修饰符 <!-- 阻止单击事件冒泡 --> <a v-on:click.stop="doThis"></a> <!-- 提 ...

  9. angularjs事件通信$on,$emit,$broadcast详解

    公司项目开发用的是angularjs,关于事件通讯一直用的是EventBus,直到上周写一个小组件懒得引用EventBus时,想到用angularjs自带的事件通信时,结果很尴尬的忘记原生方法单词怎么 ...

随机推荐

  1. ThreadLocal内存泄漏

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11421437.html 内存泄漏 内存泄漏是指不再使⽤的对象⽆法得到及时的回收,持续占⽤内存空间,从⽽ ...

  2. c++11 继承构造函数

    若基类拥有数量众多的不同版本的构造函数,而派生类中只有一些成员函数,则对于派生类而言,其构造函数就等同于构造基类. struct A { A(int i) {} A(double d, int i) ...

  3. 调用windows的复制文件对话框

    function CopyFileDir(sDirName: String; sToDirName: String): Boolean; var fo: TSHFILEOPSTRUCT; begin ...

  4. 「SCOI2016」背单词

    「SCOI2016」背单词 Lweb 面对如山的英语单词,陷入了深深的沉思,「我怎么样才能快点学完,然后去玩三国杀呢?」.这时候睿智的凤老师从远处飘来,他送给了 Lweb 一本计划册和一大缸泡椒,然后 ...

  5. poi各种jar包作用和导入

    poi各种jar包作用和导入 目前POI的最新发布版本是poi-bin-3.17-20170915. 下载地址: Apache POI - Download Release Artifacts  ht ...

  6. ROS的使用

    1.输入roscore时出现错误:Unable to contact my own server at 修改: 在.bashrc文件中添加以下内容: export ROS_HOSTNAME=local ...

  7. Hive 数据类型转换(转)

    原文连接:https://www.iteblog.com/archives/892.html 在<Hive内置数据类型>文章中,我们提到了Hive内置数据类型由基本数据类型和复杂数据类型组 ...

  8. JS-动态加载

    var s = document.createElement('script'); s.setAttribute('src', ''); s.setAttribute('type', 'text/ja ...

  9. jQuery.Deferred exception: a.indexOf is not a function TypeError: a.indexOf is not a function

    jq版本问题 3.2换成1.9就可以了

  10. Oracle之Group by和Having-----转了

    在介绍GROUP BY 和 HAVING 子句前,我们必需先讲讲sql语言中一种特殊的函数:聚合函数,例如SUM, COUNT, MAX, AVG等.这些函数和其它函数的根本区别就是它们一般作用在多条 ...