一、初始位置

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

今天来研究下 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. 【leetcode】701. Insert into a Binary Search Tree

    题目如下: Given the root node of a binary search tree (BST) and a value to be inserted into the tree, in ...

  2. 【leetcode】994. Rotting Oranges

    题目如下: In a given grid, each cell can have one of three values: the value 0 representing an empty cel ...

  3. axios拦截设置和错误处理

    目前想出的处理接口请求进行全局错误提示 的最佳方案,axios整体配置如下 1.基于axiso.interceptors进行响应拦截: 主要负责全局提示错误 axios.interceptors.re ...

  4. Vue - 前端本地项目开发过程中webpack打包内存泄漏问题解决方法

    编译项目出现如下错误: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 原因: n ...

  5. 继续我们的学习。这次鸟哥讲的是LVM。。。磁盘管理 最后链接文章没有看

    LVM...让我理解就是一个将好多分区磁盘帮到一起的玩意,类似于烙大饼...然后再切 新建了一个虚拟机,然后又挂了一个5G的硬盘,然后分出了5块空间,挂载到了虚拟机上.这些步骤很简单 fdisk    ...

  6. [CSP-S模拟测试]:斯诺(snow)(数学+前缀和+树状数组)

    题目传送门(内部题37) 输入格式 第一行一个整数$n$,表示区间的长度. 第二行一个长度为$n$的只包含$0,1,2$的字符串,表示给出的序列. 输出格式 一行一个整数,表示革命的区间的数量. 样例 ...

  7. 使用struts2未登录,不能操作

    1.定义拦截器类: 注意登录的action,登录成功在session存入标记(login) import com.opensymphony.xwork2.ActionContext; import c ...

  8. PHP导出大量数据到csv表

    对于做后台开发的码农来说,从excel导入数据到数据库亦或者是从数据库导出数据到excel都是很常见的操作.由于经常遇到这样的场景,也因为从数据库导出数据到表格所遇到的坑有很多,所以需要另辟途径来进行 ...

  9. MacBook Pro 快捷键2

    Mac 键盘快捷键 您可以按下组合键来实现通常需要鼠标.触控板或其他输入设备才能完成的操作.   要使用键盘快捷键,请按住一个或多个修饰键,同时按快捷键的最后一个键.例如,要使用快捷键 Command ...

  10. NGINX-二级域名

    先给二级域名添加到 DNS 解析再配置 nginx server { #侦听80端口 listen 80; #定义使用 www.nginx.cn访问 server_name ~^(?<subdo ...