eventsMixin
发布-订阅模式
Vue实例为调度中心,$on订阅事件,$emit发布事件,然后调度中心开始处理订阅回调,$off取消订阅,$once订阅一次事件
vm.$on订阅事件vm.$once订阅一次事件vm.$off取消订阅vm.$emit发布事件
export function eventsMixin (Vue) {
const hookRE = /^hook:/
/**
* 监听指定事件,入参为事件名或事件名组成的数组,传数组则表示监听多个事件
* vm.$on('eventName', fn)
*
* @param { string | Array<string> } event 函数名|函数名数组
* @param {*} fn 监听的回调
*/
Vue.prototype.$on = function (event, fn) {
// vm 为事件总线,即调度中心
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] || (vm._events[event] = [])).push(fn)
if (hookRE.test(event)) {
// hook监听
vm._hasHookEvent = true
}
}
return vm
}
/**
* 订阅一次,实际上就是执行 vm.$on 并在执行回调时取消订阅
* vm.$once('eventName', fn)
*
* @param { string } event
* @param {*} fn
*/
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
}
/**
* 取消订阅
* vm.$off('eventName') 取消该事件名下所有的订阅
* vm.$off('eventName', fn) 取消该事件名下指定的订阅
*
* @param { undefined | string | Array<string> } event 需要取消订阅的函数或函数数组
* @param {*} fn
*/
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) {
// !cbs 表示该事件下不存在订阅
return vm
}
if (!fn) {
// 不传fn,则表示取消该事件下所有的订阅
vm._events[event] = null
return vm
}
let cb
let i = cbs.length
// 遍历该事件下所有回调,取消指定的回调
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) { // cb.fn === fn 是指 vm.$once 时,cb 指向的是重写的回调,cb.fn 才是真实回调
cbs.splice(i, 1)
break
}
}
return vm
}
/**
* 发布事件,触发执行指定的事件回调
* vm.$emit('eventName', arg1, arg2, ...)
*
* @param { string } event 触发事件名
*/
Vue.prototype.$emit = function (event) {
const vm = this
if (process.env.NODE_ENV !== 'production') {
// 事件名大小写警告
...
}
// 获取该事件名下所有的订阅回调
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// 获取参数 args = [arg1, arg2, ...]
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
// 遍历并执行回调,invokeWithErrorHandling 内部执行
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
function invokeWithErrorHandling(handler, context, args) {
let res
try {
// 这里在上下文环境中执行订阅的事件
res = args ? handler.apply(context, args) : handler.call(context)
...
} catch (e) {
...
}
return res
}