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 事件上。
總結
在模板編譯階段會以屬性的形式存在,在真實節點渲染階段會根據事件屬性去綁定相關的事件。對於組件的自定義事件來說,我們可以用事件進行父子組件間的通信,其實質是在子組件內部自己派發事件,監聽事件。能達到通信的效果,是因為回調函數是在父組件中聲明的。