VUE源码——事件机制
VUE是怎么样处理事件的
在日常的开发中,我们把 @click 用的飞起,组件自定义事件实现父子组件之间的通信,那我们有想过其中的实现原理是什么呢?接下来我们将探索原生事件和自定义事件的奥秘。带着疑问开始撸源码。
首先来点儿测试代码,在测试代码中,我们包含了原生的事件,和自定义事件
<body> <div id="app"> <h1>事件处理</h1> <!-- 普通事件 --> <p @click='onclick'>普通事件</p> <!-- 自定义事件 --> <comp @myclick="onMyClick"></comp> </div> </body> <script> Vue.component('comp', { template: `<div @click="onClick">this is comp</div>`, methods: { onClick() { this.$emit('myclick') } } }) const app = new Vue({ el: '#app', methods: { onclick() { console.log('普通事件'); }, onMyClick() { console.log('自定义事件'); } }, }) console.log(app.$options.render); </script>
在Vue 挂载之前做了许多编译的工作,把 template 模板编译成 render函数,这个过程就不做过多的讲解。我们主要来看生产render函数后是怎么实现事件的绑定的。
我们来观察打印出的app.$options.render 的结果
(function anonymous() { with(this) { return _c('div', { attrs: { "id": "app" } }, [_c('h1', [_v("事件处理")]), _v(" "), _c('div', { on: { "click": onclick } }, [_v("普通事件")]), _v(" "), _c('comp', { on: { "myclick": onMyClick } })], 1) } })
根据打印的结果来看,普通事件和自定义事件生成的结果其实差不多,都将事件的处理放在了on上面。
普通事件
据我所知,在Vue 组件初始化的时候,原生事件的监听会在 platforms\web\runtime\modules\events.js里面,会执行 updateDOMListeners方法。
想要知道验证一下,是否执行到了该函数,我们可以在函数里面打断点验证一下。
可以看到,我们会成功的进入,那想要知道调用流程,我们可以在堆栈信息里面看看。
因为在有了 Vnode 过后,会遍历子节点递归的调用 createElm 为每个子节点创建真实的 DOM,在创建真实的 DOM 时会组成相关的钩子invokeCreateHooks。其中就包括注册事件的处理 updateDOMListeners 。
进入到 invokeCreateHooks 函数
function invokeCreateHooks (vnode, insertedVnodeQueue) { for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { cbs.create[i$1](emptyNode, vnode); } i = vnode.data.hook; // Reuse variable if (isDef(i)) { if (isDef(i.create)) { i.create(emptyNode, vnode); } if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); } } }
我们可以看看会执行哪些钩子函数。
我们可以看到,在 invokeCreateHooks 函数里面,是把所有的钩子函数执行一遍,其中就有 updateDOMListeners 。
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} target = vnode.elm // 兼容性处理 normalizeEvents(on) updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) target = undefined }
其中 normalizeEvents 是对 v-model 的兼容性处理,在 IE 下没有 input 只支持 change 事件,把 input 事件替换成 change 事件。
if (isDef(on[RANGE_TOKEN])) { // IE input[type=range] only supports `change` event const event = isIE ? 'change' : 'input' on[event] = [].concat(on[RANGE_TOKEN], on[event] || []) delete on[RANGE_TOKEN] }
updateListeners 的逻辑也不复杂,它会遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听。
export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event for (name in on) { ... // 执行真正注册事件的执行函数 add(event.name, cur, 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) } } }
add 函数,是在真正的 DOM 上绑定事件,它的实现也是利用了原生 DOM 的 addEventListener
function add ( name: string, handler: Function, capture: boolean, passive: boolean ) { ... target.addEventListener( name, handler, supportsPassive ? { capture, passive } : capture ) }
一目了然,将事件添加到原生的click事件上,并实现了监听。 以上就是普通事件绑定的流程。
自定义事件
我们知道,父子组件可以使用事件进行通信,子组件通过vm.$emit 向父组件派发事件,父组件通过v-on:(event)接受信息并处理回调。
从最开始的例子中可以看出,普通节点使用的原生DOM事件,在组件上可以使用自定义事件,另外组件上还可以使用原生事件,用 .native 修饰符区分。 接下来我们看看自定义事件是怎么处理的。
在 Vnode 生成真实节点的过程中,这个过程遇到子Vnode会实例化子组件实例。实例化子类构造器的过程,会有初始化选项配置的过程,会进入到Vue.prototype.init,我们直接看对自定义事件的处理。 在 src\core\instance\init.js中
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ ... // merge options // 针对子组件的事件处理 if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) // 初始化事件处理 initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') ... }
进入到事件处理函数 initEvents, 里面的处理逻辑也是比较简单,就几行代码。
export 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) } }
我们把断点打到函数里面看一看
第一次进去的时候,我们看到当前创建的是根组件,根组件的 _uid:0, 我们放过再进一次,现在看到的就是我们自定义的组件在创建。这时候的 listeners 会存在。
接下来会进去 updateComponentListeners,自定义事件的处理。
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined }
简单的看这段代码,把当前组件实例赋值给目标对象 target, 然后进行事件监听。
同样的,会有 add 函数执行,那这里的 add 和原生事件的又所不同,我们可以猜想一下,这里的 add 是怎么处理的。
export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event for (name in on) { ... } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } add(event.name, cur, 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) } } }
可能也猜想到了,是通过 $on 进行事件监听
function add (event, fn) { target.$on(event, fn) }
我们可以看到,自定义事件,虽然是在事件监听声明在父组件上来,但是监听还是在子组件上监听的,所义谁派发,谁监听。
那会存在疑问,自己派发,自己监听,那是怎么和父组件经通信的呢? 这里需要注意下,回调函数是在父组件声明的。
我们会想,子组件是怎么拿到父组件的自定义事件的呢 ,其实在updateComponentListeners 中 vm.$options._parentListeners,可以拿到父组件的自定义事件。那么 _parentListeners 又是怎么来的呢?
其实在 _init 方法里,执行 initEvents 之前,会对组件进行处理。initInternalComponent(vm, options)
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
在父组件里面的组件的 vnodeComponentOptions里面的 listeners就是自定义组件里面定义的事件,myClick, 这样在子组件内部就可以拿到,然后在绑定在 on 事件上。
总结
在模板编译阶段会以属性的形式存在,在真实节点渲染阶段会根据事件属性去绑定相关的事件。对于组件的自定义事件来说,我们可以用事件进行父子组件间的通信,其实质是在子组件内部自己派发事件,监听事件。能达到通信的效果,是因为回调函数是在父组件中声明的。