本章代码位于vue/src/core/instance/events.js
紧跟着生命周期之后的就是继续初始化事件相关的属性和方法。整个事件系统的代码相对其他模块来说非常简短,分几个部分来详细看看它的具体实现。
头部引用
1 2 3 4 5 6 7 8
| import { tip, toArray, hyphenate, handleError, formatComponentName } from '../util/index' import { updateListeners } from '../vdom/helpers/index'
|
头部先是引用了的一些工具方法,没有什么难点,具体可以查看相应文件。唯一值得注意的是引用自虚拟节点模块的一个叫 updateListeners
方法。顾名思义,是用来更新监听器的,至于为什么要有这样的一个方法,主要是因为如果该实例的父组件已经存在一些事件监听器,为了正确捕获到事件并向上冒泡,父级事件是需要继承下来的,这个原因在下面的初始化代码中有佐证;另外,如果在实例初始化的时候绑定了同名的事件处理器,也需要为同名事件添加新的处理器,以实现同一事件的多个监听器的绑定。
事件初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } }
let target: any
function add (event, fn, once) { if (once) { target.$once(event, fn) } else { target.$on(event, fn) } }
function remove (event, fn) { target.$off(event, fn) }
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, vm) target = undefined }
|
如上述代码所示,事件监听系统的初始化首先是创建了私有的事件对象和是否有事件钩子的标志两个属性,然后根据父级是否有事件处理器来决定是否更新当前实例的事件监听器,具体如何实现监听器的更新,贴上这段位于虚拟节点模块的辅助函数中的代码片段来仔细看看。
更新事件监听器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
export 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) 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) } } }
|
这段代码中用到了 normalizeEvent
和 createFnInvoker
两个主要的函数来完成更新监听器的实现,代码与 updateListeners
函数位于同一文件中。
normalizeEvent
:主要是用于返回一个定制化的事件对象,这个函数接受4个必选参数和2两个可选参数,分别是事件名称name属性、是否一次性执行的once属性、是否捕获事件的capture属性、是否使用被动模式passive属性、事件处理器handler方法、事件处理器参数params数组。属性的含义都比较好理解,特别注意一下 once
、capture
、passive
属性,这三个属性是用来修饰事件的,分别对应了 ~
、!
、&
修饰符,贴上一个官方文档中的使用示例,引用自事件 & 按键修饰符。启动被动模式的用途是使事件处理器无法阻止默认事件,比如 <a>
标签自带的链接跳转事件,如果设置passive为true,则事件处理器即便是设置了阻止默认事件也是没办法阻止跳转的。1 2 3 4 5
| on: { '!click': this.doThisInCapturingMode, '~keyup': this.doThisOnce, '~!mouseover': this.doThisOnceInCapturingMode }
|
createFnInvoker
: 接受一个fns参数,可以传入一个事件处理器函数,也可以传入一个包含多个处理器的数组。在该函数内部定义了一个 invoker
函数并且最终返回它,函数有一个fns属性是用来存放所传入的处理器的,调用这个函数后,会按fns的类型来分别执行处理器数组的调用或单个处理器的调用。这个实现即是真正执行事件处理器调用的过程。
事件相关的原型方法
在事件的初始化过程里有用到几个以 &
开头的类原型方法,它们是在mixin函数里挂载到核心类上的。初始化的时候定义的方法都是在这些方法的基础上再进行了一次封装,其绑定事件、触发事件和移除事件的具体实现都在这些方法中,当然不会放过对这些细节的探索。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ 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 { (vm._events[event] || (vm._events[event] = [])).push(fn) if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } 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 } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = 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++) { this.$off(event[i], fn) } return vm } const cbs = vm._events[event] if (!cbs) { return vm } if (!fn) { vm._events[event] = null return vm } if (fn) { let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } } return vm } 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 } }
|
eventsMixin的内容非常直观,分别为实例原型对象挂载了$on
、$once
、$off
、$emit
四个方法。这是实例事件监听函数的注册、一次性注册、移除和触发的内部实现。在使用的过程中会对这些实现有一个更清晰的理解。
终于对Vue的事件系统的实现有了一个大致了解,没有什么特别高深的处理,但完整的事件系统的实现有很多细致的功能这里其实并没有特别详细地探讨,比如事件修饰符,可以参考官方文档里的解说会有一个更清晰的了解。事件系统的重要作用首先是为实例制定了一套处理事件的方案和标准,其次是在实例数据更新的过程中保持对事件监听器的更新,这两个部分的处理是最需要细致去琢磨的。