Vue源码学习之initEvents
initLifecycle是Vue源码中core/instance/events.js下的一个函数,和上节的initLiftcycle一样,该函数也是在beforeCreate钩子之前调用,作用是初始化组件中的事件。下面让我们来进行代码分析。
1、initEvents
function initEvents (vm: Component) {
// 存放事件的空对象
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events(初始化父组件绑定在该组件上的事件)
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
这个函数的功能就是初始化了一个存放事件的空对象,只存放挂载在该组件上的事件。_hasHookEvent属性是表示父组件是否有将钩子函数绑定到该组件上。如果父组件有绑定事件到该组件上则调用updateComponentListeners方法,下面看一下该方法的实现。
2、updateComponentListeners
function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, vm)
target = undefined
}
这个方法没有什么内容,主要就是调用updateListeners更新Listener,比较重要的就是add和remove两个参数,这两个方法是Vue中自己实现两个添加Listener、移除Listener的方法。
3、add
function add (event, fn, once) {
if (once) {
target.$once(event, fn)
} else {
target.$on(event, fn)
}
}
可以看到该方法主要依赖的是 once两个vue实例上的方法,这两个方法还有 off方法都是通过一个eventsMixin方法挂载到Vue原型对象上的,下面让我们了解一下这两个方法的实现。
$on
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
// 如果是数组的话,对数组中的每一项进行绑定
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
// _events对象属性中存入相应事件
(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
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
$on方法主要就是将事件push到vue实例的_events对象对应事件属性下。
$once
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
off方法卸载,再执行我们要执行的函数,这样就只调用一次了
回到刚才的add方法,可以发现add方法就是通过调用 once方法进行添加事件。
4、remove
function remove (event, fn) {
target.$off(event, fn)
}
remove的本质是通过 off方法。
$off
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all(不传参数为清空全部事件,直接将_events对象置空)
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events(event为数组则一个一个删除)
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
// 如果只传入第一个参数,将该属性下的所有事件清空
if (!fn) {
vm._events[event] = null
return vm
}
if (fn) {
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
// cb.fn是使用$once注册的情况
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
}
return vm
}
通过代码可以看出$off有三种用法,第一种:不传参数,将所有事件清空;第二种:只传第一个参数,将_events对象下的该属性事件数组清空;第三种:传入两个参数,清除某个特定事件。
$emit
介绍完了 once, emit方法,$emit是触发事件的方法,直接看代码吧。
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}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
忽略前面的一大段关于环境的判断,可以看出$emit方法就是直接触发vue实例下的_events对象中的方法。
5、updateListeners
function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
}
add(event.name, cur, event.once, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
简单的说就是往vue实例中添加on对象内事件,移除oldOn下的事件。