之前我们已经介绍了event
的编译过程(点击这里跳转),接下来我们分析在Vue
初始化和更新的过程中event
的内部是如何生成的。
event生成之自定义事件
Vue
中event
事件分为原生DOM
事件与自定义事件,原生DOM
事件的处理(点击这里跳转),我们上一节已经分析过了。这一节我们来分析下自定义事件。
自定义事件是用在组件节点上的,组件节点上定义的事件可以分为两类:一类是原生DOM
事件( 在vue2.x
版本在组件节点上使用原生DOM事件需要添加native
修饰符),另一类就是自定义事件。
下面我们来分析自定义事件的流程:
创建组件vnode
创建组建vnode
(虚拟节点)的时候会执行createComponent
函数,其中有如下逻辑:
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void { ...... // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners // 自定义事件赋值给listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. // native事件赋值给data.on,这样原生方法直接就上一节相同的逻辑了 data.on = data.nativeOn ...... // return a placeholder vnode // 创建占位符vnode const name = Ctor.options.name || tag // 生成虚拟节点的时候,将listeners当参数传入 const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) // 返回vnode return vnode}
创建组件vnode
的过程中会将组件节点上的定义的自定义事件赋值给listeners
变量,同时将组件节点上定义的原生事件赋值给data.on
属性,这样,组件的原生事件就会执行如同上一节生成原生事件相同的逻辑。然后在创建组件vnode
的时候,会将listeners
(缓存了自定义事件)当做第七个参数(componentOptions
)的属性值。
vnode
创建完成之后,在初始化组件的时候,会执行initInternalComponent
函数:
组件初始化
initInternalComponent
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { // 子组件构造器的options(配置项) const opts = vm.$options = Object.create(vm.constructor.options) // .... // 我们之前创建的节点的第七个参数(componentOptions) const vnodeComponentOptions = parentVnode.componentOptions // 子组件构造器的_parentListeners属性指向之前定义的listeners(组件自定义事件) opts._parentListeners = vnodeComponentOptions.listeners // ...}
执行完这些配置项的生成之后,会初始化子组件事件
export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events const listeners = vm.$options._parentListeners // 有listeners,执行updateComponentListeners if (listeners) { updateComponentListeners(vm, listeners) }}
listeners
非空,执行updateComponentListeners
函数:
let target: anyexport function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object) { // target指向当前实例 target = vm // 执行updateListeners updateListeners(listeners, oldListeners || {}, add, remove, vm) target = undefined}
这个地方同样执行updateListeners
函数,与上一节原生DOM事件的生成相同,但与原生DOM
事件的生成有几处不同之处,如下add
与remove
函数的定义。
function add (event, fn, once) { if (once) { // 如果有once属性,执行$once方法 target.$once(event, fn) } else { 否则执行$on方法 target.$on(event, fn) }}function remove (event, fn) { // remove方法是执行$off方法 target.$off(event, fn)}
关于$once
、$on
、$off
函数都定义在eventsMixin
中:
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { ...... } Vue.prototype.$once = function (event: string, fn: Function): Component { ...... } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { ...... } Vue.prototype.$emit = function (event: string): Component { ...... }}
$on
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { // 当前实例就是调用该方法的实例 const vm: Component = this // 如果event是数组,遍历数组,依次执行$on函数 if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$on(event[i], fn) } } else { // 将当前实例的_events属性初始化为空数组并push当前添加的函数 (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
的逻辑就是将当前的方法存入当前实例vm._events
属性中。
$once
Vue.prototype.$once = function (event: string, fn: Function): Component { // 当前实例就是调用该方法的实例 const vm: Component = this // 定义on函数 function on () { // 执行$off销毁当前事件 vm.$off(event, on) // 执行函数fn fn.apply(vm, arguments) } // on的fn属性指向当前传入的函数 on.fn = fn // 将on函数存入vm._events中 vm.$on(event, on) return vm}
$once
的逻辑就是对传入的fn
函数做了一层封装,生成了一个内部函数on
,on.fn
属性指向传入函数fn
,将on
函数存入实例的_events
属性对象中,这样执行完一次这个函数后,该函数就被销毁了。
$off
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { // 当前实例就是调用该方法的实例 const vm: Component = this // all // 如果没有传参数,将vm._events置为空对象 if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events // event如果是数组,遍历该数组,依次调用$off函数 if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$off(event[i], fn) } // 返回 return vm } // specific event // 唯一的event const cbs = vm._events[event] // cbs未定义,直接返回 if (!cbs) { return vm } // fn未定义(未传入fn的情况下),vm._events[event]赋值为空,直接返回 if (!fn) { vm._events[event] = null return vm } // fn定义了 if (fn) { // specific handler let cb let i = cbs.length // 遍历cbs对象 while (i--) { cb = cbs[i] // 如果查找到有属性与fn相同 if (cb === fn || cb.fn === fn) { // 移除该属性,跳出循环 cbs.splice(i, 1) break } } } return vm }
$off
的作用就是移除vm._events
对象上定义的事件函数。
eventsMixin
中还定义了一个函数$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}".` ) } } // 拿到vm._events的event事件上的所有函数 let cbs = vm._events[event] // 存在cbs if (cbs) { // cbs转化 cbs = cbs.length > 1 ? toArray(cbs) : cbs // 其他参数转化成数组 const args = toArray(arguments, 1) // 遍历cbs,依次执行其中的函数 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
开发过程中常用的eventBus
的实现,原理也是同上。
到此为止,关于Vue
的event
原理已经大致介绍完毕了,欢迎交流探讨。