vue源码解析事件处理机制

1,509 阅读5分钟

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)
}