Vue 初始化之 initEvents
上一篇看了 Vue 初始化生命周期的一些内外部的属性,这篇继续往下看,来看看 initEvents,初始化事件相关。
在 Vue 中,在父组件中使用子组件时可以给子组件上注册一些事件,这些事件即包括使用 v-on 或@注册的自定义事件,以及一些浏览器原生的事件(.native)。
export function initEvents(vm: Component) {
// 创建一个原型为null的新对象,用来存放父组件绑定在当前组件上的事件
vm._events = Object.create(null);
// 父组件是否通过 "@hook:XXX" 把钩子函数绑定在当前组件上
vm._hasHookEvent = false;
// 初始化父组件添加的事件
// _parentListeners 就是父组件注册的事件
const listeners = vm.$options._parentListeners;
if (listeners) {
// 如果存在,执行 updateComponentListeners
updateComponentListeners(vm, listeners);
}
}
initEvents 在初始化的时候,创建一个空对象来存放父组件绑定的事件,把_hasHookEvent 设置为 false,判断父组件注册的事件,如果有,就执行 updateComponentListeners。但是在 Vue 初始化的时,没有父组件,updateComponentListeners 在初始化子组件的时候执行。
不过我们也来看一下 updateComponentListeners 这个函数
let target: any;
export function updateComponentListeners(
vm: Component,
listeners: Object,
oldListeners?: Object | null,
) {
target = vm;
updateListeners(
listeners,
oldListeners || {},
add,
remove,
createOnceHandler,
vm
);
target = undefined;
}
从上面的代码来看,updateListeners 就是调用了 updateListeners 方法,传入了一些参数。我们来看看这些参数:
function add(event, fn) {
// 执行 vm.$on 方法
target.$on(event, fn);
}
add 方法其实就是执行了 vm.$on 方法,$on 方法在 eventsMixin 函数里
const hookRE = /^hook:/;
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
// 缓存this
const vm: Component = this;
// 如果 event 是数组,那就遍历的调用本身来处理
if (isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
// 把 event 和回调 fn 传入到 _events 中
(vm._events[event] || (vm._events[event] = [])).push(fn);
// 这个bool标志位来表明是否存在钩子,
// 而不需要通过哈希表的方法来查找是否有钩子,
// 这样做可以减少不必要的开销,优化性能
// 主要是为了判断有没有父组件的@hook:这样的钩子事件
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
// 返回vm
return vm;
};
通过上面的代码,我们可以看到,Vue.$on事件主要就是做了一件事,就是把event 放到_events数组里,等emit的时候去调用。
function remove(event, fn) {
// 执行 vm.$off 方法,取消 event 的 fn 的回调
target.$off(event, fn)
}
remove方法调用了$off方法,我们看看 $off 方法
Vue.prototype.$off = function (
event?: string | Array<string>,
fn?: Function
): Component {
// 缓存this
const vm: Component = this
// 如果没有传递参数的话,则移除所有注册的事件和回调函数
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// 如果 event 是数组的话,则遍历 event 数组,并依次调用 this.$off(event[i], fn)
if (isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// 单个的 event 事件,通过 vm._events[event] 获取 event 事件的回调函数数组
const cbs = vm._events[event!]
// 如果 cbs 不存在的话,说明没有注册 event 事件,直接 return 返回即可
if (!cbs) {
return vm
}
// 如果没有回调函数,就把当前事件的所有回调都删除
if (!fn) {
vm._events[event!] = null
return vm
}
// 删除某个回调 fn
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
// 返回vm
return vm
}
$off 方法就是删除 _events 里的某个事件某个回调或者全部回调函数
createOnceHandler 函数,是用来处理.once 修饰符修饰的事件的
// 创建只执行一次的 once 事件,返回一个函数体
function createOnceHandler(event, fn) {
// 缓存 target
const _target = target
return function onceHandler() {
// 调用定义的回调
const res = fn.apply(null, arguments)
if (res !== null) {
// 如果返回值不为 null 则取消监听
// 返回 null 则 .once 修饰符失效
_target.$off(event, onceHandler)
}
}
}
这个函数创建一个只执行一次的 once 事件,返回一个函数,去执行事件的回调。
接下来,我们来看updateListeners这个函数。
export function updateListeners(
on: Object, // listeners
oldOn: Object, // old listener
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, cur, old, event
// 遍历新 listeners 的事件
for (name in on) {
cur = on[name] // 获取新 listener 里的 事件
old = oldOn[name] // 获取旧 listener 里的 事件
event = normalizeEvent(name) // 规范化 event
// 如果 cur 不存在
if (isUndef(cur)) {
// 非生产环境 报错提示
__DEV__ &&
warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
// 第一次创建
// 如果 当前事件 的 fns 属性不存在
if (isUndef(cur.fns)) {
// 给 cur 和 on[name] 都创建一个回调
cur = on[name] = createFnInvoker(cur, vm)
}
// 如果带 once 操作符
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
// 调用传入的add函数
// updateComponentListeners 和 updateDOMListeners 调用的 add 方法不同,
// updateDOMListeners 里的 add 方法在 src/platforms/web/runtime/modules/events.ts
// updateComponentListeners 里的 add 方法 src/core/instance/events.ts
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
// 更新时触发, 条件是 cur 和 old 不同
// 修改回调函数
old.fns = cur
// 保留引用关系
on[name] = old
}
}
// 在更新阶段
// 如果新 listeners 中没有当前事件名
// 删除旧 listeners 里的当前事件
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
// 删除 name 对应的方法
remove(event.name, oldOn[name], event.capture)
}
}
}
这个函数的作用是对比listeners和oldListeners的不同,并调用参数中提供的add和remove进行相应的注册事件和卸载事件。
首先遍历新的 listeners,通过name规范化event
// 判断事件是否有 once、capture、passive 等修饰符
const normalizeEvent = cached(
(
name: string
): {
name: string
once: boolean
capture: boolean
passive: boolean
handler?: Function
params?: Array<any>
} => {
const passive = name.charAt(0) === '&'
name = passive ? name.slice(1) : name
const once = name.charAt(0) === '~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) === '!'
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
}
)
拿到规范化的event的name 之后,就去判断,如果新的事件在旧的 listeners里没有,代表第一次注册,就createFnInvoker 生成一个 invoker。
// 创建一个 invoker,然后 invoker 的 fns 属性等于 fns
export function createFnInvoker(
fns: Function | Array<Function>,
vm?: Component
): Function {
function invoker() {
const fns = invoker.fns
// 回调是数组
if (isArray(fns)) {
// 缓存一下fns
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(
cloned[i],
null,
arguments as any,
vm,
`v-on handler`
)
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(
fns,
null,
arguments as any,
vm,
`v-on handler`
)
}
}
invoker.fns = fns
return invoker
}
createFnInvoker函数定义了一个invoker函数,给invoker函数添加了一个属性fns,属性值就是定义事件的回调函数;最终返回invoker函数并赋值给on[name]
如果有 once 修饰符 ,就去调用传入的 createOnceHandler 生成只执行一次的handler
// 如果带 once 操作符
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
对于第一次创建会调用传入的add函数,并通过$on 给components 绑定事件。而这里的 add 方法有4个参数,则是在处理 updateDOMListeners 的时候,对应 addEventListener 来给DOM 添加事件。
对于在 old 里存在的事件, 则是更新的时候:
// 更新时触发, 条件是 cur 和 old 不同
else if (cur !== old) {
// 修改回调函数
old.fns = cur
// 保留引用关系
on[name] = old
}
遍历old的所有事件,如果新事件中没有当前事件名,则通过remove删除之前绑定的事件。
// 如果新 listeners 中没有当前事件名
// 删除旧 listeners 里的当前事件
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
// 删除 name 对应的方法
remove(event.name, oldOn[name], event.capture)
}
}
总结:
这篇文章主要来了解初始化Vue组件的事件,包括:@事件和自定义事件等,分析了initEvents函数的实现。了解vm 实例event 的 绑定,接触,以及存储的方式,事件都放在_events属性里,等$emit的时候,去触发回调函数