vue中我们监听的事件有两种,一种是原生事件的监听,一种是自定义事件的监听。如下所示
<!--普通事件-->
<p @click="onClick">this is p</p>
<!--自定义事件-->
<comp @myclick="onMyClick"></comp>
@click中的click为原生事件@myclick的myclick为自定义事件。
本会会先讲解一下原生事件和自定义事件实现的基本原理,再从源码角度进行更详细的解析。
基本原理
原生事件基本实现原理
<!--普通事件-->
<p @click="onClick">this is p</p>
- 第一步,
<p @click="onClick">this is p</p>作为模板,通过解析模版我们知道其上面有一个click事件,并且对应的回调函数是onClick。 - 第二步,创建一个真实的p元素,并使用addEventListener(name,handler)添加监听事件,其中name为
click,handler为当前实例上的onClick方法。即p.addEventListener('click',vm.onClick)。
自定义事件基本实现原理
<!--自定义事件-->
<comp @myclick="onMyClick"></comp>
- 第一步,
<comp @myclick="onMyClick"></comp>作为模板,通过解析模版我们知道其上面有一个myclick事件,并且对应的回调函数是onMyClick。 - 第二步,创建组件实例
compVm。 - 第三步,组件实例监听
myclick事件,并且设置其对应的回调函数是父组件的onMyClick方法,即compVm.$on('myclick', parentVm.onMyClick),当组件内部使用compVm.$emit('myclick')触发myclick方法时,会执行父组件实例的onMyClick方法。
源码解析
原生事件实现源码解析
<!--普通事件-->
<p @click="onClick">this is p</p>
从我们new Vue实例开始,到最终p标签监听click方法,整体流程图如下(看个大概即可):

从上图可以看出,从new Vue实例到执行挂载,最终会进入到创建p标签的流程,创建p元素之后会触发invokeCreateHooks方法,该方法会遍历元素创建相关的钩子函数并执行,来更新函数上的属性和事件。
createElm核心代码
// 进行p元素的创建
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 创建p元素的子节点
createChildren(vnode, children, insertedVnodeQueue)
// 执行p元素与创建相关的钩子函数
invokeCreateHooks(vnode, insertedVnodeQueue)
// 将p元素插入到父级dom的对应位置中
insert(parentElm, vnode.elm, refElm)
invokeCreateHooks核心代码
cbs.create为一个数组,其里面包含了dom创建完成后需要执行的回调函数,用来更新dom上的属性和事件等,其回调函数包括:
- updateAttrs(oldVnode, vnode) 更新普通属性
- updateClass(oldVnode, vnode) 更新class
- updateDOMListeners(oldVnode, vnode) 更新监听事件
- updateDOMProps(oldVnode, vnode) 更新传递到dom里的数据,比如input里的value
- updateStyle(oldVnode, vnode) 更新样式
- updateDirectives(oldVnode, vnode) 更新指令
// 取出创建元素相关的所有回调函数执行一遍
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](vnode)
}
我们今天要研究的即更新监听事件的钩子函数 updateDOMListeners(oldVnode, vnode)
updateDOMListeners核心代码
// 取出vnode.data.on里面保存的要监听的事件
const on = vnode.data.on || {}
// 调用updateListeners方法,并将on作为参数传进去
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
vnode.data.on里保存了我们要监听的事件,要监听哪些事件是在解析模版的时候获得的,获取要监听的事件的基本原理如下:
- 模版:
<p @click="onClick">this is p</p> - 对应的渲染函数:
_c('p',{on:{"click":onClick}
解析模版生成的渲染函数如上所示,解析得到的要监听的事件都会保存在on对象里面,后面会遍历on对象里面的参数进行事件的监听。
updateListeners核心代码
// 遍历on里面的事件进行监听,从上面的分析可以知道,on为类似{"click":onClick}这样的对象
for (name in on) {
add(event.name, cur, event.capture, event.passive, event.params)
}
add核心代码
// 这里的target即为的p元素,name即为`click`事件,`handler`即为对应的回调函数onClick
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
以上即为Vue处理原生事件的源码解析 ~
自定义事件实现源码解析
<!--自定义事件-->
<comp @myclick="onMyClick"></comp>
这里直接放一个源码调试过程中调用栈的图,从图中可以看到从new Vue()到最终调用add函数进行组件事件的监听的整个流程。

new Vue() ==> add 流程解析
- 1、调用
new Vue()进行根实例的创建 - 2、调用
Vue._init进行初始化 - 3、调用封装的
$mount获取渲染函数 - 4、调用内层的
$mount实行挂载 - 5、调用
mountCompount方法,该方法会创建updateComponent更新函数和根实例的Watcher - 6、创建
Watcher - 7、
Watcher会调用内部的get方法 - 8、
get方法会调用updateComponent方法 - 9、
updateComponent方法会调用_render方法获取虚拟dom,并调用_update方法 - 10、
_update方法执行patch - 11、
patch方法对新旧虚拟dom进行比较 - 12、根实例创建时不存在旧虚拟dom,所以会根据新虚拟dom并调用createElm创建真实dom
- 13、
createElm创建真实dom的过程会使用createChildren向下递归创建子节点 - 14、递归到我们的组件节点,对组件节点调用
createElm创建其真实dom - 16、调用
createComponent创建组件 - 17、调用组件的初始化钩子函数
init - 18、调用
createComponentInstanceForVnode,创建组件的实例 - 19、使用组件构造函数
VueComponent创建组件实例 - 20、调用
_init方法进行实例的初始化 - 21、
_init里会调用事件初始化函数initEvents - 22、
initEvents会调用updateComponentListeners - 23、
updateComponentListeners会调用updateListeners - 24、
updateListeners里会遍历所有要监听的方法并调用add - 25、add方法会调用
$on方法进行事件的监听
整个流程的调用非常的复杂,这里挑组件上自定义事件的关键源码进行解析。首先看下我们组件的模版编译成渲染函数是怎么样的
- 模版:
<comp @myclick="onMyClick"></comp> - 渲染函数:
_c('comp',{on:{"myclick":onMyClick}})
这里和原生事件类似,会把组件上自定义的事件解析出来都保存在一个on对象里面,最终添加事件监听的时候会用到这个on对象。
当我们要创建组件dom的时候,会先创建组件实例,即上面的第19步,然后会调用_init初始化方法,我们从这个方法开始理一下其源码结构:
_init核心代码
initLifecycle(vm) // $parent, $root, $children, $refs
// 重点!事件处理在这个里面,其它的初始化相关的方法可不管
initEvents(vm) // 对父组件传入事件添加监听
initRender(vm) // 声明$slots,$createElement()
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
initInjections(vm) // 注入数据
initState(vm) // 重要:数据初始化,响应式
initProvide(vm) // 提供数据
callHook(vm, 'created')
initEvents核心代码
export function initEvents (vm: Component) {
// init parent attached events
// 取出父元素上定义的方法,即上面说的on对象
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
updateComponentListeners核心代码
// 把target设为当前组件实例,updateListeners里会有用
target = vm
// 调用updateListeners
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
updateListeners核心代码
export function updateListeners (
on: Object,
) {
// 遍历on里面要监听的事件
// 在这里on类似为{'myclick': onMyClick}
// 这里的onMyClick指向父组件里的onMyClick方法
for (name in on) {
// cur为事件对应的回调函数
cur = on[name]
// 每个要监听的事件调用add方法
add(event.name, cur)
}
}
add核心代码
这里的target即为updateComponentListeners赋值的组件实例,fn指向父组件里对应的方法,当我们在组件内部使用$emit(event)的时候,即会调用父组件里的对应方法
function add (event, fn) {
target.$on(event, fn)
}