Vue原理解析——事件系统
Vue中的事件分为Dom事件和Vue自定义事件两种。这两者的区别是什么呢?
Dom事件
v-on作为Dom事件的指令入口,要经过模板编译和指令注册两部分。先看编译部分:
const onRE = /^v-on:|^@/
//编译元素中的指令
function compileDirectives (attrs, options) {
...
//关于“事件绑定”的处理
if (onRE.test(name)) {
arg = name.replace(onRE, '')
//添加一个“on”指令, publicDirectives.on为该指令的所有“钩子函数”等属性和方法
//“钩子函数”的介绍可参考Vue文档中自定义指令的介绍
pushDir('on', publicDirectives.on)
}
...
}
在publicDirectives.on中定义了on指令的所有属性和方法,其中的update方法进行了事件的绑定处理,如下:
update (handler) {
// "v-on"属性值为空的容错处理,
// 例如 @mousedown.prevent
if (!this.descriptor.raw) {
handler = function () {}
}
// "v-on"属性值不为空但不是函数,报错
if (typeof handler !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'v-on:' + this.arg + '="' +
this.expression + '" expects a function value, ' +
'got ' + handler,
this.vm
)
return
}
// 事件修饰符(modifiers)处理
if (this.modifiers.stop) {
// 返回一个新的方法,先执行event.stopPropagation(),再执行事件绑定事件
handler = stopFilter(handler)
}
if (this.modifiers.prevent) {
// 返回一个新的方法,先执行event.preventDefault(),再执行事件绑定事件
handler = preventFilter(handler)
}
if (this.modifiers.self) {
// 返回一个新的方法,如果触发元素为当前元素,才执行事件绑定事件
handler = selfFilter(handler)
}
// 事件按键修饰符处理
var keys = Object.keys(this.modifiers)
.filter(function (key) {
//排除事件修饰符
return key !== 'stop' &&
key !== 'prevent' &&
key !== 'self' &&
key !== 'capture'
})
if (keys.length) {
// 返回一个新的方法,如果触发元素的keyCode等于按键修饰符中的code,才执行事件绑定事件
handler = keyFilter(handler, keys)
}
// 避免重复绑定,先解绑
this.reset()
this.handler = handler
if (this.iframeBind) {
//处理iframe中的事件绑定
this.iframeBind()
} else {
//调用on方法进行事件绑定
on(
this.el,
this.arg,
this.handler,
this.modifiers.capture
)
}
}
最后的on方法就是原生Dom二级事件的绑定了。
on (el, event, cb, useCapture) {
el.addEventListener(event, cb, useCapture)
}
自定义事件
自定义事件只能绑定在Vue对象上,可以使用v-on指令,也可以使用Vue提供的四个事件API去处理:$on,$once,$off,$emit。
前面我们说过Vue初始化入口 ,在其中就包含对事件对象的初始化方法_initEvents。
initEvents (vm: Component) {
/*在vm上创建一个_events对象,用来存放事件。*/
vm._events = Object.create(null)
/*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
vm._hasHookEvent = false
// init parent attached events
/*初始化父组件attach的事件*/
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
然后通过$on方法将事件处理函数推入_events对象中管理。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
/*如果是数组的时候,则递归$on,为每一个成员都绑定上方法*/
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
(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
/*这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
$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++) {
cbs[i].apply(vm, args)
}
}
return vm
}
可以看到Vue自定义事件的原理就是观察者模式,通过在Vue实例维护一个事件对象去管理即可。
-- EOF --