这段时间一直在看vue的源码,源码很是多和杂,因此本身结合资料和理解理出了一个主线,而后根据主线去剥离其余的一些知识点,而后将各个知识点逐一学习。这里主要是分析的事件系统的实现。vue
在分析以前先了解下几个api的使用方式:json
vm.$on(event, callback)
参数:api
{string | Array<string>} event
(数组只在 2.2.0+ 中支持){Function} callback
$on
事件须要两个参数,一个是监听的当前实例上的事件名,一个是事件触发的回调函数,回调函数接受的是在事件出发的时候额外传递的参数。vm.$on('test', function (msg) { console.log(msg) }) vm.$emit('test', 'hi') // => "hi"
vm.$once(event, callback)
$once
事件总体上来讲和$on
事件的使用方式差很少,可是event只支持字符串也就是说只支持单个事件。而且该事件再触发一次后就移除了监听器。数组
vm.$once('testonce', function (msg) { console.log(msg) })
vm.$off([event, callback])
参数:app
{string | Array<string>} event(仅在 2.2.2+ 支持数组)
{Function} [callback]
用法:移除自定义事件监听器函数
vm.$off() vm.$off('test') vm.$off('test1', function (msg) { console.log(msg) }) vm.$off(['test1','test2'], function (msg) { console.log(msg) })
vm.$emit(event, [..args])
参数:源码分析
{string} event
要触发的事件名[...args]
可选触发当前实例上的事件。附加参数都会传给监听器回调。学习
vm.$emit('test', '触发自定义事件')
事件的初始化工做
咱们在使用自定义事件的api的时候,确定有个地方是须要来存咱们的事件和回调的地方。在vue
中大部分的初始化工做都是在core/instance/init.js
的initMixin
方法中。因此咱们可以在initMixin
看到initEvents
方法。this
// 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) } }
上面的代码能够看到,在初始化Vue
事件的时候,在vm
实例上面挂载了一个_events
的空对象。后面咱们本身调用的自定义事件都存在里面。prototype
由于vue自己在组件嵌套的时候就有子组件使用父组件的事件的时候。因此就能够经过updateComponentListeners
方法把父组件事件监听器(好比click)传递给子组件。(这里不作过多讨论)
自定义事件的挂载方式
自定义事件的挂载是在eventsMixin
方法中实现的。这里面将四个方法挂在Vue的原型上面。
Vue.prototype.$on Vue.prototype.$once Vue.prototype.$off Vue.prototype.$emit
Vue.prototype.$on的实现
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) // 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 }
第一个参数就是自定义事件,由于多是数组,因此判断若是是数组,那么就循环调用this.$on
方法。
若是不是数组,那么就直接向最开始定义的_events
对象集合里面添加自定义事件。
因此这个时候_events
对象生成的格式大概就是下面:
vm._events={ 'test':[fn,fn...], 'test1':[fn,fn...] }
Vue.prototype.$once 的实现
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 }
这里定义了一个on
函数。接着把fn
赋值给on.fn
。最后在调用的是vm.$on
。这里传入的就是事件名和前面定义的on
函数。on
函数在执行的时候会先移除_events
中对应的事件,而后调用fn
因此分析下获得的是:
vm._events={ 'oncetest':[ function on(){ vm.$off(event,on) fn.apply(vm,arguments) } , ... ] }
Vue.prototype.$off的实现
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // all // 若是没有传任何参数的时候,直接清楚全部挂在_events对象上的全部事件。 if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events // 若是第一个参数是数组的话,那么就循环调用this.$off方法 if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$off(event[i], fn) } return vm } // specific event // 获取对应事件全部的回调多是个数组 const cbs = vm._events[event] // 没有相关的事件的时候直接返回vm实例 if (!cbs) { return vm } // 若是只传入了事件名,那么清除该事件名下全部的事件。 也就是说 vm._events = {'test': null, ...} if (!fn) { vm._events[event] = null return vm } // 若是传入的第二个参数为真,那么就去cbs里面遍历,在cbs中找到和fn相等的函数,而后经过splice删除该函数。 if (fn) { // specific handler 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 的实现
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}".` ) } } // 匹配到事件列表,该列表是一个json。 let cbs = vm._events[event] if (cbs) { //将该json转化成为真正的数组 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 }
上面主要意思是:匹配到json中相关key值的value,这个value先转换成真正的数组,再循环遍历数组,传入给的参数执行数组中的每一个函数
vue中的自定义事件主要目的是为了组件之间的通讯。由于_events
对象是挂在Vue实例上的。所以每一个组件是均可以访问到vm._events
的值的,也可以向其中push
值的。
整个自定义事件系统呢就是在vm实例上挂载一个_events的对象,能够理解为一个json,其中json的key值就是自定义事件的名称,一个key值可能对应着多个自定义事件,所以json中每一个key对应的value都是一个数组,每次执行事件监听都会向数组中push相关的函数,最终经过$emit函数传入的参数,匹配到json中相应的key,val值,从而使用给定的参数执行数组中的函数。
最后的_events
对象:
vm._events={ 'test1':[fn,fn,fn], 'test2':[fn], 'oncetest':[ function on(){ vm.$off(event,on) fn.apply(vm,arguments) }, ... ], ... }