Vue 事件分为两类:原生DOM事件、自定义事件。其中原生DOM事件既能够在元素上使用,也能够在组件上使用,在组件上使用时要添加.native修饰符。
Vue 经过调用原生API来处理元素和组件上绑定的原生DOM事件,在组件上的自定义事件则是由基于发布/订阅模式的事件中心机制完成的。
前端
手动实现一个遵循发布/订阅模式的事件中心比较简单,代码以下所示:
vue
function EventEmitter() {
this._events = Object.create(null)
}
EventEmitter.prototype.$on = function(type, handler) {
(this._events[type] || (this._events[type] = [])).push(handler)
}
EventEmitter.prototype.$off = function(type, handler) {
var events = this._events[type]
if (events) {
var cb
var i = events.length
while (i--) {
cb = events[i]
if (cb === handler) {
events.splice(i, 1)
break
}
}
}
}
EventEmitter.prototype.$emit = function(type) {
var args = toArray(arguments, 1)
if (this._events[type]) {
this._events[type].forEach(fn => fn.apply(this, args))
}
}
EventEmitter.prototype.$once = function(type, handler) {
var _this = this
var flag = false
function on() {
_this.$off(type, on)
if (!flag) {
flag = true
handler.apply(_this, arguments)
}
}
_this.$on(type, on)
}
export default EventEmitter
复制代码
订阅方法都保存在实例对象的 _events 属性中,自定义事件的名称为 _events 的属性,其值为数组类型,存储着事件触发后的回调函数。
经过调用 $on 方法来订阅事件,实现方式是将回调函数推入 _events 对象上的对应属性数组中。
经过调用 $off 方法来移除自定义事件监听器,实现方式是将回调函数从 _events 对象上的对应属性数组中删除。
经过调用 $emit 方法来触发对应事件,本质上是将对应订阅方法取出执行。
经过调用 $once 方法来添加一个只触发一次的自定义事件,实现过程当中利用一个标识变量来判断方法是否触发。
vuex
在上篇文章《指令》讲到 v-on 指令时,关于在组件上使用自定义指令的部分没有阐述其具体实现。仅仅说到自定义事件的添加与删除最终是调用了实例方法 $on 与 $off 来完成的,自定义事件的触发是调用实例方法 $emit 来完成。
在实例化Vue对象时会调用其构造函数:
数组
function Vue (options) {
/* 省略警告信息 */
this._init(options)
}
/* 省略其它mixin */
eventsMixin(Vue)
Vue.prototype._init = function (options) {
const vm = this
/* 省略... */
initEvents(vm)
}
复制代码
储存订阅方法的对象在 initEvents 方法中定义,实例事件方法在 eventsMixin 中定义。
app
function initEvents (vm) {
vm._events = Object.create(null)
/* 省略... */
}
function eventsMixin (Vue) {
const hookRE = /^hook:/
Vue.prototype.$on=function(event,fn){/*省略*/}
Vue.prototype.$once=function(event,fn){/*省略*/}
Vue.prototype.$off=function(event,fn){/*省略*/}
Vue.prototype.$emit=function(event){/*省略*/}
}
复制代码
首先来看 $on 的具体实现:
函数
Vue.prototype.$on = function(event,fn){
const vm = this
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] = [])).push(fn)
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
复制代码
能够看到 Vue 源码中的 $on 跟咱们上一节手动实现的核心代码是一致的,只是 Vue 的 $on 方法第一个参数能够为数组,所以在函数开始进行这种状况的处理:若是是数组,则循环调用 $on 方法,以数组中的值为第一个参数。
若是第一个参数 event 与 /^hook:/ 正则匹配时,将实例对象的 _hasHookEvent 属性置为 true,这跟生命周期钩子函数有关,相关细节将在下一篇文章《生命周期》中阐述。
post
Vue.prototype.$off=function(event,fn){
const vm = this
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
const cbs = vm._events[event]
if (!cbs) { return vm }
if (!fn) {
vm._events[event] = null
return vm
}
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
复制代码
在 $off 方法中对了许多边界条件的处理,好比不传任何参数调用该方法则将 vm._events 变量置空,即移除全部的事件监听器;若是第一个参数是数组则循环调用 $off 方法;若是只提供第一个参数,则将 vm._events[event] 置空,即移除该事件全部的监听器。
能够看到,$off 方法的核心实现中比咱们手动实现的多了一个条件:cb.fn === fn。事件的 fn 属性与要删除的方法相同也执行删除操做,之因此加上这一个条件是为了配合 $once 方法的实现。
学习
Vue.prototype.$once=function(event, fn){
const vm = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
复制代码
$once 方法是经过调用 $on 方法实现的,只是将回调包裹在内部函数 on 中,在触发 $on 方法够会调用内部函数中的 $off 方法移除该事件监听,从而实现了 $once 方法监听一个自定义事件,可是只触发一次的功能。
ui
Vue.prototype.$emit=function(event){
const vm = this
/* 省略警告信息 */
let cbs = vm._events[event]
if (cbs) {
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++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
复制代码
$emit 方法的实现与上一节手动实现的原理同样,就是执行存储在 _events 中的对应方法,只是 Vue 经过调用 invokeWithErrorHandling 进行了一些错误处理。
this
EventBus 即为事件总线,能够很方便的实现非父子组件间通讯。EventBus 在 Vue 中的具体实现就是经过事件中心机制实现的。
// event-bus.js
import Vue from 'vue'
const bus = new Vue()
export default bus
// A 组件
import bus from '@/event-bus.js'
bus.$on('CONSOLE', number => {
console.log(number)
})
// B 组件
import bus from '@/event-bus.js'
bus.$emit('CONSOLE', 1)
// C 组件
import bus from '@/event-bus.js'
bus.$off('CONSOLE')
复制代码
EventBus 实现简单,操做便捷,具备很高的灵活性。但就是由于过于灵活,若是在项目中随意使用,那后期维护起来将是灾难。
若是有不少地方须要进行非父子组件间的通讯,最正确的选择是使用 vuex 进行状态管理,关于 vuex 的原理阐述将在后续文章进行介绍。
Vue 中对自定义事件的处理是经过基于发布/订阅模式的事件中心机制实现的,核心思路就是将事件存储到实例的一个属性对象上,事件的添加、删除、触发等操做都是在该对象上进行的。
欢迎关注公众号:前端桃花源,互相交流学习!