Vue源码阅读(24):v-on 指令的源码解析_纷飞丿的博客-程序员宅基地_v-on原理

技术标签: vue源码阅读系列  前端  vue.js  源码  

今天讲讲 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 指令。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/f18855666661/article/details/119917796

智能推荐

Android 逆向笔记 —— 一个简单 CrackMe 的逆向总结_u012551350的博客-程序员宅基地

温馨提示请拖动到文章末尾,长按识别「抽奖」小程序。在我的印象中,懂逆向的,都是大牛,让我们一起来看看下面这位大牛的学习心得。无意中在看雪看到一个简单的 CrackMe 应...

快速生成二维码_可以吧可以吧的博客-程序员宅基地_快速产生qrcode方法

在只能手机如此普及的今天,二维码作为推广的的展现,应用的越来越多了。一个二维码中可以蕴藏很多信息。那么,就让我来介绍一下,如何在 thinkphp5 框架中生成二维码。下载类库前往https://packagist.org搜索 phpqrcode ,我选择的是composer require aferrandini/phpqrcode打开 cmd 进入项目根目录,通过 co...

机器人_Bears9的博客-程序员宅基地

链接:https://www.nowcoder.com/acm/contest/159/B来源:牛客网 机器人时间限制:C/C++ 1秒,其他语言2秒空间限制:C/C++ 131072K,其他语言262144K64bit IO Format: %lld题目描述从前在月球上有一个机器人。月球可以看作一个 n*m 的网格图,每个格子有三种可能:空地,障碍,机器人(有且仅有一个...

phpStudy 80端口被进程占用无法启动Apache_Rihaong_yyy的博客-程序员宅基地

问题描述:打开phpStudy后,弹出一个“80端口被占用”的窗口。随后,没过多考虑点了“中止”选项,启动的时候运行状态“Apache停止运行”;启动失败的原因:防火墙拦截 80端口被别的程序占用 ( √ ) 没有安装VC9运行库,php和apache都是VC9编译解决方案:1. 查看端口使用情况:运行cmd, netstat -ano 找到80端口对应的PID...

chrome.runtime.sendMessage 回调函数参数为undefined_henryzyk的博客-程序员宅基地_chrome.runtime.sendmessage

chrome.runtime.sendMessage 回调函数参数为undefinedchrome.runtime.sendMessage的回调函数默认是同步的,而且超时后直接执行,返回undefined,如果要异步执行,必须在处理函数中return true//background.jschrome.runtime.onMessage.addListener(function (request,sender,callback) { // 异步方法 return true; /

JavaScript DOM文档对象模型_遇见是缘的博客-程序员宅基地

DOM文档对象模型1.DOM,全称Document Object Model文档对象模型,JS中通过DOM来对HTML文档进行操作。只要理解了DOM就可以随心所欲的操作WEB页面。文档表示的就是整个的HTML网页文档。对象表示将网页中的每一个部分都转换为了一个对象。使用模型来表示对象之间的关系,这样方便我们获取对象。2.节点Node,是构成我们网页的最基本的组成部分,网页中的每一个部分都可以称...

随便推点

短信验证码发送失败的常见原因有哪些?_亿美emay的博客-程序员宅基地_验证码短信发送失败是什么原因

短信验证码现在几乎已成为互联网各行业的标配所在,在账户注册、密码修改、支付确认等方面发挥着重要的作用。目前通过短信验证码接口接入第三方短信验证码平台的短信服务,99%以上的用户基本上都可以在几秒钟之内就顺利接收到验证码,但是也会出现极少数用户短信验证码收不到的情况。那么如果短信验证码下发失败收取不到时,常见的原因有哪些呢?通常情况下,短信验证码收不到的原因大致上可从下面这几个方面来进行分析:一、企业原因1、企业在设计短信验证码发送内容时,要注意短信内容中是否出现违禁词语。2、短信验证码签名.

聊城大学c语言实验报告,c语言程序设计(包云)c第1章概述.ppt_范不易cool的博客-程序员宅基地

c语言程序设计(包云)c第1章概述.pptC语言程序设计,讲授包云 单位聊城大学计算机学院,第1章 C语言概述,3,主要内容,1.1 什么是计算机程序 1.2 什么是计算机语言 1.3 C语言的发展及其特点 1.4 简单的C程序介绍 1.5 C程序的上机步骤与方法,4,1.1什么是程序设计,什么是程序 为了解决某一特定问题用某一种计算机语言编写的指令序列称为程序。 什么是程序设计 程序是程序设计的...

自测代码(方法与面向对象的应用【求区间质数、求100个随机数中的最小数、阶乘、图书管理类的应用】)-2021-7-21_戟御的博客-程序员宅基地_用面向对象实习区间的质数

MyClass类package com.qianfeng.day07;public class MyClass { //输出三角形的行数 public void printTriangle(int n) { for (int i = 1; i &lt;= n; i++) { for (int j = 0; j &lt; 2*i-1; j++) { System.out.print("*"); } System.out.println(); } } //

朴素贝叶斯和贝叶斯估计_weixin_30563319的博客-程序员宅基地

贝叶斯定理贝叶斯定理是关于随机事件A和B的条件概率和边缘概率的一则定理。在参数估计中可以写成下面这样:这个公式也称为逆概率公式,可以将后验概率转化为基于似然函数和先验概率的计算表达式,即在贝叶斯定理中,每个名词都有约定俗成的名称:P(A)是A的先验概率或边缘概率。之所以称为"先验"是因为它不考虑任何B方面的因素。P(A|B)是已知B发生后A的条件概率(在B发生...

Vue + Element-UI使用_白三岁吖.的博客-程序员宅基地

Vue + Element-UI使用安装elementcnpm i element-ui -S在vue当中引入element-ui在main.js中写入以下内容import ElementUI from 'element-ui';import 'element-ui/lib/theme-chalk/index.css';Vue.use(ElementUI);用element布局页面1、使用container布局页面,首页引入导航, Index.vue&lt;template

安卓修改电池容量教程_Android 使用adb查看和修改电池信息_weixin_39757893的博客-程序员宅基地

1、获取电池信息$ adb shell dumpsys battery$ adb shell dumpsys batteryCurrent Battery Service state:AC powered:false        //false表示没使用AC电源USB powered: true        //true表示使用USB电源Wireless powered: false     ...

推荐文章

热门文章

相关标签