Vue源码阅读(24):v-on 指令的源码解析

 我的开源库:

今天讲讲 v-on 指令的底层实现原理。在 Vue 中,v-on 指令有两种用法,第一种是将 v-on 指令使用在自定义组件上,例如:<my-component v-on:myEvent="doSomething"></my-component>,使用 v-on 指令监听了组件的 myEvent 事件,回调函数是 doSomething,当在组件中执行 this.$emit('myEvent') 时,会触发执行 doSomething 函数,有没有发现这和我上一篇文章中的 vm.$on 和 vm.$emit 挺像的,没错,他们的底层实现原理的确很相似。第二种用法是将 v-on 指令用在原生的元素上,可以实现在 DOM 元素上绑定原生的事件,例如 <div v-on:click="doSomething"></div>,当点击该 div 元素时,会触发执行 doSomething 回调函数。接下来,对这两种用法分别进行解析。

1,v-on 在自定义组件上使用

看这一小节前,最好先看看我的上一篇文章

我们以下面的代码为例。

Vue.component('component-a', {
  template: '<h1>我是组件</h1>'
})

new Vue({
  el: '#app',
  data() {
    return {
    }
  },
  methods: {
    nameChangeHandler(){
      console.log("name change handler")
    }
  },
  template: `
    <div id="app">
      <component-a @nameChange="nameChangeHandler"></component-a>
    </div>
  `
})

1-1,模板字符串 >>> vnode

生成的 vnode 如下所示,children 数组中的第一个 vnode 元素就是使用的 component-a 组件对应的 vnode,该 vnode 的 componentOptions.listeners 属性对象保存着在父节点中给当前组件绑定的事件和回调函数。

在 patch() 方法进行页面的渲染时,如果发现处理的 vnode 节点是组件节点的话,则会进入创建组件的逻辑,每个组件都有一个 Vue 实例与之对应。

1-2,createComponentInstanceForVnode

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
  parentElm?: ?Node,
  refElm?: ?Node
): Component {
  const vnodeComponentOptions = vnode.componentOptions
  // options 是创建组件 vue 实例的参数对象
  const options: InternalComponentOptions = {
    _isComponent: true,
    parent,
    propsData: vnodeComponentOptions.propsData,
    _componentTag: vnodeComponentOptions.tag,
    _parentVnode: vnode,
    // 将 vnode.componentOptions.listeners 赋值到 options._parentListeners
    _parentListeners: vnodeComponentOptions.listeners,
    _renderChildren: vnodeComponentOptions.children,
    _parentElm: parentElm || null,
    _refElm: refElm || null
  }
  // 创建组件对应的 vue 实例,并且以 options 为参数
  return new vnodeComponentOptions.Ctor(options)
}

1-3,Vue.prototype._init

上面调用的 new vnodeComponentOptions.Ctor(options) 就是下面的 VueComponent 函数,该函数的内部会执行 _init 函数。

const Sub = function VueComponent (options) {
  this._init(options)
}

_init 函数中简要代码如下:

Vue.prototype._init = function (options?: Object) {
  // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
  const vm: Component = this

  initInternalComponent(vm, options)

  // 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法。
  initEvents(vm)
}


function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)

  // 将 options._parentListeners 保存到 vm.$options._parentListeners
  opts._parentListeners = options._parentListeners
}

_init 函数内部首先执行 initInternalComponent(vm, options) 函数,该函数的作用是将 options 中的众多属性保存到 vm.$options 中,其中就包括 _parentListeners 属性,initInternalComponent 函数执行完成后,会执行 initEvents(vm),对在父组件绑定到子组件上的事件和响应函数进行处理。

1-4,initEvents 函数

export function initEvents (vm: Component) {
  // 初始化 vue 实例中的 _events 属性,该属性用于保存:
  // 1,vm.$on() 绑定的事件和响应函数。
  // 2,父组件在子组件上绑定的事件和响应函数。
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 获取父组件在子组件上绑定的事件集合对象,{ eventName1: callback1, ...... }
  const listeners = vm.$options._parentListeners
  // 如果父组件的确在子组件上绑定了事件的话,执行 updateComponentListeners 函数
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

1-5,updateComponentListeners 函数

在 updateComponentListeners 函数中,会调用 updateListeners 函数,这里的 add 和 remove 方法通过调用上一篇博客中的 $once、$on、$off 完成功能。也就是说,在组件上使用 v-on 指令绑定事件及回调函数的底层和 vm.$on 函数一样,都是将事件和回调函数存储到 vm._events 中,且都可以在子组件中通过 this.$emit("eventName") 抛出对应的事件,进而触发执行对应的回调函数。

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
}

// 添加事件,借助 $once 和 $on 完成功能
function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

// 移除事件,借助 $off 完成功能
function remove (event, fn) {
  target.$off(event, fn)
}

1-6,updateListeners 函数

该函数的解释都在注释中,看注释即可。

// 对比 on 与 oldOn,然后根据对比的结果调用 add 方法或者 remove 方法执行绑定或解绑事件
// 该函数的一大特点是:add 和 remove 函数与 updateListeners 函数解耦,它们作为参数传递到
// updateListeners 方法中,updateListeners 方法主要做 on 与 oldOn 的比较。
export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  let name, cur, old, event
  // 遍历处理 on 中的事件
  for (name in on) {
    // 根据事件名称(例如:click)获取对应的回调函数
    cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    if (isUndef(cur)) {
      // 如果 cur 回调函数未定义的话,说明没有给这个事件绑定回调函数,打印出警告
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      // 如果 old 回调函数未定义,cur 回调函数定义了的话,说明当前的事件是新增的
      // 需要执行 add 方法进行事件的绑定
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur)
      }
      add(event.name, cur, event.once, event.capture, event.passive)
    } else if (cur !== old) {
      // 如果 cur 和 old 都定义了,并且 cur !== old 的话,则执行到这里
      //
      // 执行到这里的情形是:之前和现在 DOM 元素都绑定了 name 事件,但是绑定的回调函数不一样,
      // 所以需要对执行的回调函数进行更新。更新的方式也很简单,将 cur 赋值到 old.fns 即可,
      // 至于为什么这样就能改变绑定的回调函数,看 createFnInvoker 函数的源码注释
      old.fns = cur
      on[name] = old
    }
  }
  // 遍历处理 oldOn 中的事件
  for (name in oldOn) {
    // 如果当前遍历的事件在 on 中不存在的话
    // 说明该事件以前绑定了,而最新的状态没有绑定,此时需要执行 remove 进行该事件的解绑操作
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

这里,还需要特别注意的一点是:上述的处理都是在初始化组件 Vue 实例的阶段做的事。

2,v-on 在 DOM 元素上使用

在讲这部分内容之前,建议先看看我的这篇博客 — Vue源码阅读(19):自定义指令的源码解析 中的第一小节知识补充部分。

在 patch 的过程中,除了更新 DOM 元素中的内容外,还会更新 DOM 很多其他的东西,例如:directives、ref、attrs、class、events、style 等,更新 events 的代码定义在 src/platforms/web/runtime/modules/events.js 文件中,我们直接看源码。

/* @flow */

import { isDef, isUndef } from 'shared/util'
import { updateListeners } from 'core/vdom/helpers/index'
import { withMacroTask, isIE, supportsPassive } from 'core/util/index'
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'

let target: HTMLElement

// 实现事件修饰符 .once 的方法
function createOnceHandler (handler, event, capture) {
  const _target = target // save current target element in closure
  // 返回一个包装函数,当事件触发的时候,执行的就是这个返回的包装函数
  // 该包装函数执行时,内部会触发执行真正的回调函数,回调函数执行一次后,
  // 后续元素就不需要再绑定 event 事件了,所以执行 remove 方法解绑 event 事件
  return function onceHandler () {
    const res = handler.apply(null, arguments)
    if (res !== null) {
      // res !== null 的时候,才会执行 remove 方法,这主要是为了解决一个 bug,
      // issues 看这里:https://github.com/vuejs/vue/issues/4846
      remove(event, onceHandler, capture, _target)
    }
  }
}

// 绑定事件
function add (
  event: string,
  handler: Function,
  once: boolean,
  capture: boolean,
  passive: boolean
) {
  handler = withMacroTask(handler)
  // 如果事件绑定使用了 .once 的话,则给 handler 加一层包装
  if (once) handler = createOnceHandler(handler, event, capture)
  // 这里只是调用浏览器提供的 API --- node.addEventListener 绑定事件
  // target 就是使用了 v-on 指令的 DOM 元素
  target.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

// 解绑事件
function remove (
  event: string,
  handler: Function,
  capture: boolean,
  _target?: HTMLElement
) {
  // 调用浏览器提供的 API --- node.removeEventListener 解绑事件
  (_target || target).removeEventListener(
    event,
    handler._withTask || handler,
    capture
  )
}

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  // 如果 oldVnode 和 vnode 中都没有事件对象的话,说明之前没有绑定任何事件,现在也没有新增绑定事件
  // 因此不需要做事件的绑定和解绑操作,直接 return 即可。
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  // 获取 vnode 和 oldVnode 中的事件对象
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  // vnode.elm 是 vnode 在页面上对应的真实 DOM 节点
  target = vnode.elm
  normalizeEvents(on)
  // 更新元素上的事件
  // 内部的机制是:对比 on 与 oldOn,然后根据对比的结果调用 add 方法或者 remove 方法执行绑定或解绑事件
  updateListeners(on, oldOn, add, remove, vnode.context)
}

export default {
  create: updateDOMListeners,
  update: updateDOMListeners
}

该文件的最后导出了一个对象,对象中有两个属性,分别是 create 和 update,这说明当 DOM 元素刚创建和更新的时候都会触发执行后面的 updateDOMListeners,该函数用于更新 DOM 元素的事件绑定。

updateDOMListeners 函数的具体解释看注释即可,该函数的最后也会执行 updateListeners 函数,只不过作为参数的 add 和 remove 是当前文件中独有的,用于绑定和解绑 DOM 元素上的事件。updateListeners 函数的内容看上面的 1-6 小节。

add 函数的最后通过执行原生的 node.addEventListener() 方法绑定事件,而 remove 函数通过执行 node.removeEventListener() 方法解绑事件。而且,当我们绑定事件时,使用了 once 事件修饰符的话,还会进行一层 createOnceHandler 函数的处理,这部分源码的详细解释看注释。

3,结语

以上就是 v-on 指令的底层实现原理,下一篇博客讲 v-model 指令。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值