【Vue2.0源码学习】生命周期篇-初始化阶段(initEvents)

2023-10-30

1. 前言

本篇文章介绍生命周期初始化阶段所调用的第二个初始化函数——initEvents。从函数名字上来看,这个初始化函数是初始化实例的事件系统。我们知道,在Vue中,当我们在父组件中使用子组件时可以给子组件上注册一些事件,这些事件即包括使用v-on@注册的自定义事件,也包括注册的浏览器原生事件(需要加 .native 修饰符),如下:

<child @select="selectHandler" 	@click.native="clickHandler"></child>

不管是什么事件,当子组件(即实例)在初始化的时候都需要进行一定的初始化,那么本篇文章就来看看实例上的事件都是如何进行初始化的。

2. 解析事件

我们先从解析事件开始说起,回顾之前的模板编译解析中,当遇到开始标签的时候,除了会解析开始标签,还会调用processAttrs 方法解析标签中的属性,processAttrs 方法位于源码的 src/compiler/parser/index.js中, 如下:

export const onRE = /^@|^v-on:/
export const dirRE = /^v-|^@|^:/

function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, value, modifiers
  for (i = 0, l = list.length; i < l; i++) {
    name  = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      // 解析修饰符
      modifiers = parseModifiers(name)
      if (modifiers) {
        name = name.replace(modifierRE, '')
      }
      if (onRE.test(name)) { // v-on
        name = name.replace(onRE, '')
        addHandler(el, name, value, modifiers, false, warn)
      }
    }
  }
}

从上述代码中可以看到,在对标签属性进行解析时,判断如果属性是指令,首先通过 parseModifiers 解析出属性的修饰符,然后判断如果是事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn) 方法, 该方法定义在 src/compiler/helpers.js 中,如下:

export function addHandler (el,name,value,modifiers) {
  modifiers = modifiers || emptyObject

  // check capture modifier 判断是否有capture修饰符
  if (modifiers.capture) {
    delete modifiers.capture
    name = '!' + name // 给事件名前加'!'用以标记capture修饰符
  }
  // 判断是否有once修饰符
  if (modifiers.once) {
    delete modifiers.once
    name = '~' + name // 给事件名前加'~'用以标记once修饰符
  }
  // 判断是否有passive修饰符
  if (modifiers.passive) {
    delete modifiers.passive
    name = '&' + name // 给事件名前加'&'用以标记passive修饰符
  }

  let events
  if (modifiers.native) {
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }

  const newHandler: any = {
    value: value.trim()
  }
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers
  }

  const handlers = events[name]
  if (Array.isArray(handlers)) {
    handlers.push(newHandler)
  } else if (handlers) {
    events[name] = [handlers, newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

addHandler 函数里做了 3 件事情,首先根据 modifier 修饰符对事件名 name 做处理,接着根据 modifier.native 判断事件是一个浏览器原生事件还是自定义事件,分别对应 el.nativeEventsel.events,最后按照 name 对事件做归类,并把回调函数的字符串保留到对应的事件中。

在前言中的例子中,父组件的 child 节点生成的 el.eventsel.nativeEvents 如下:

el.events = {
  select: {
    value: 'selectHandler'
  }
}

el.nativeEvents = {
  click: {
    value: 'clickHandler'
  }
}

然后在模板编译的代码生成阶段,会在 genData 函数中根据 AST 元素节点上的 eventsnativeEvents 生成_c(tagName,data,children)函数中所需要的 data 数据,它的定义在 src/compiler/codegen/index.js 中:

export function genData (el state) {
  let data = '{'
  // ...
  if (el.events) {
    data += `${genHandlers(el.events, false,state.warn)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true, state.warn)},`
  }
  // ...
  return data
}

生成的data数据如下:

{
  // ...
  on: {"select": selectHandler},
  nativeOn: {"click": function($event) {
      return clickHandler($event)
    }
  }
  // ...
}

可以看到,最开始的模板中标签上注册的事件最终会被解析成用于创建元素型VNode_c(tagName,data,children)函数中data数据中的两个对象,自定义事件对象on,浏览器原生事件nativeOn

在前面的文章中我们说过,模板编译的最终目的是创建render函数供挂载的时候调用生成虚拟DOM,那么在挂载阶段, 如果被挂载的节点是一个组件节点,则通过 createComponent 函数创建一个组件 vnode,该函数位于源码的 src/core/vdom/create-component.js 中, 如下:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // ...
  const listeners = data.on

  data.on = data.nativeOn

  // ...
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

可以看到,把 自定义事件data.on 赋值给了 listeners,把浏览器原生事件 data.nativeOn 赋值给了 data.on,这说明所有的原生浏览器事件处理是在当前父组件环境中处理的。而对于自定义事件,会把 listeners 作为 vnodecomponentOptions 传入,放在子组件初始化阶段中处理, 在子组件的初始化的时候, 拿到了父组件传入的 listeners,然后在执行 initEvents 的过程中,会处理这个 listeners

所以铺垫了这么多,结论来了:父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。

换句话说:实例初始化阶段调用的初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。

3. initEvents函数分析

了解了以上过程之后,我们终于进入了正题,开始分析initEvents函数,该函数位于源码的src/instance/events.js中,如下:

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

可以看到,initEvents函数逻辑非常简单,首先在vm上新增_events属性并将其赋值为空对象,用来存储事件。

vm._events = Object.create(null)

接着,获取父组件注册的事件赋给listeners,如果listeners不为空,则调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件的实例中,如下:

const listeners = vm.$options._parentListeners
if (listeners) {
  updateComponentListeners(vm, listeners)
}

这个updateComponentListeners函数是什么呢?该函数定义如下:

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

function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

function remove (event, fn) {
  target.$off(event, fn)
}

可以看到,updateComponentListeners函数其实也没有干什么,只是调用了updateListeners函数,并把listeners以及addremove这两个函数传入。我们继续跟进,看看updateListeners函数干了些什么,updateListeners函数位于源码的src/vdom/helpers/update-listeners.js中,如下:

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur)
      }
      add(event.name, cur, event.once, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

可以看到,该函数的作用是对比listenersoldListeners的不同,并调用参数中提供的addremove进行相应的注册事件和卸载事件。其思想是:如果listeners对象中存在某个key(即事件名)而oldListeners中不存在,则说明这个事件是需要新增的;反之,如果oldListeners对象中存在某个key(即事件名)而listeners中不存在,则说明这个事件是需要从事件系统中卸载的;

该函数接收5个参数,分别是onoldOnaddremovevm,其中on对应listenersoldOn对应oldListeners

首先对on进行遍历, 获得每一个事件名,然后调用 normalizeEvent 函数(关于该函数下面会介绍)处理, 处理完事件名后, 判断事件名对应的值是否存在,如果不存在则抛出警告,如下:

for (name in on) {
  def = cur = on[name]
  old = oldOn[name]
  event = normalizeEvent(name)
  if (isUndef(cur)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Invalid handler for event "${event.name}": got ` + String(cur),
      vm
    )
  }
}

如果存在,则继续判断该事件名在oldOn中是否存在,如果不存在,则调用add注册事件,如下:

if (isUndef(old)) {
  if (isUndef(cur.fns)) {
    cur = on[name] = createFnInvoker(cur)
  }
  add(event.name, cur, event.once, event.capture, event.passive, event.params)
}

这里定义了 createFnInvoker 方法并返回invoker函数:

export function createFnInvoker (fns) {
  function invoker () {
    const fns = invoker.fns
    if (Array.isArray(fns)) {
      const cloned = fns.slice()
      for (let i = 0; i < cloned.length; i++) {
        cloned[i].apply(null, arguments)
      }
    } else {
      // return handler return value for single handlers
      return fns.apply(null, arguments)
    }
  }
  invoker.fns = fns
  return invoker
}

由于一个事件可能会对应多个回调函数,所以这里做了数组的判断,多个回调函数就依次调用。注意最后的赋值逻辑, invoker.fns = fns,每一次执行 invoker 函数都是从 invoker.fns 里取执行的回调函数,回到 updateListeners,当我们第二次执行该函数的时候,判断如果 cur !== old,那么只需要更改 old.fns = cur 把之前绑定的 involer.fns 赋值为新的回调函数即可,并且 通过 on[name] = old 保留引用关系,这样就保证了事件回调只添加一次,之后仅仅去修改它的回调函数的引用。

if (cur !== old) {
  old.fns = cur
  on[name] = old
}

最后遍历 oldOn, 获得每一个事件名,判断如果事件名在on中不存在,则表示该事件是需要从事件系统中卸载的事件,则调用 remove方法卸载该事件。

以上就是updateListeners函数的所有逻辑,那么上面还遗留了一个normalizeEvent 函数是干什么用的呢?还记得我们在解析事件的时候,当事件上有修饰符的时候,我们会根据不同的修饰符给事件名前面添加不同的符号以作标识,其实这个normalizeEvent 函数就是个反向操作,根据事件名前面的不同标识反向解析出该事件所带的何种修饰符,其代码如下:

const normalizeEvent = cached((name: string): {
  name: string,
  once: boolean,
  capture: boolean,
  passive: boolean,
  handler?: Function,
  params?: Array<any>
} => {
  const passive = name.charAt(0) === '&'
  name = passive ? name.slice(1) : name
  const once = name.charAt(0) === '~'
  name = once ? name.slice(1) : name
  const capture = name.charAt(0) === '!'
  name = capture ? name.slice(1) : name
  return {
    name,
    once,
    capture,
    passive
  }
})

可以看到,就是判断事件名的第一个字符是何种标识进而判断出事件带有何种修饰符,最终将真实事件名及所带的修饰符返回。

4. 总结

本篇文章介绍了生命周期初始化阶段所调用的第二个初始化函数——initEvents。该函数是用来初始化实例的事件系统的。

我们先从模板编译时对组件标签上的事件解析入手分析,我们知道了,父组件既可以给子组件上绑定自定义事件,也可以绑定浏览器原生事件。这两种事件有着不同的处理时机,浏览器原生事件是由父组件处理,而自定义事件是在子组件初始化的时候由父组件传给子组件,再由子组件注册到实例的事件系统中。

也就是说:初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。

最后分析了initEvents函数的具体实现过程,该函数内部首先在实例上新增了_events属性并将其赋值为空对象,用来存储事件。接着通过调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件实例中的_events对象里。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Vue2.0源码学习】生命周期篇-初始化阶段(initEvents) 的相关文章

  • 使用 ScriptEngine 从 JavaScript 调用 Java 方法

    我正在使用 ScriptEngine 运行 JavaScript 我希望 JavaScript 脚本能够调用 myFunction 其中 myFunction 是我的给定类中的一个方法 我知道可以将 importPackage 用于标准 J
  • 从数组数组中获取唯一值[重复]

    这个问题在这里已经有答案了 我有以下数组 let arr email protected cdn cgi l email protection email protected cdn cgi l email protection email
  • angularjs 自定义过滤器检查数据数组内的值

    我有两个过滤器 它们根据数据中的队列键过滤数据 这是我的代码 var app angular module app app controller mainController function scope Data object scope
  • 从 Angular 6 服务中绑定图像

    我有一个端点 它根据某些参数为我提供图像 这不是一个图像网址 而是一个普通图像 因此 当我到达邮递员中的端点时 作为响应 我收到一张图像 JPG 我是否可以在变量中接收该图像并将其绑定到 HTML 标签中 所有问题都有将图像 url 映射到
  • 画布图像遮罩/重叠

    在我的项目中 我必须使用画布在另一个相同尺寸和图案图像上实现一个不同的颜色图像 并且图像不是圆形或矩形形状 所有这些都是波浪形状的 它将应用于单个主背景图像 以便在每个主背景图像上显示多个图形onclick功能 重叠的图像应更改为另一种选定
  • 使用 JavaScript 填写 PDF 表单

    这就是我所拥有的 用户填写很长的 html 表单 用户获取下载不同 pdf 的链接 这是可填写的表格 链接是使用 javascript 生成的 用户单击链接 生成 url 使用用户之前提交的数据 在表单中处理数据并完成字段 这是在表单内使用
  • 使用 word_number 值对 javascript 数组进行排序

    如何对数组进行排序 var arr new Array word 12 word 59 word 17 这样我得到 word 12 word 17 word 59 Thanks 您需要编写一个排序方法 您可以编写任何您喜欢的方法 该方法在
  • TypeScript 中类和命名空间的区别

    到底有什么区别classes and namespaces在打字稿中 我知道 如果您创建一个带有静态方法的类 您可以在不实例化该类的情况下访问它们 这正是我猜想的命名空间的要点之一 我还知道你可以创建多个同名的命名空间 并且它们的方法在编译
  • Javascript 根据字段值任意排序数组

    所以我有一个对象数组 如下所示 var myArray priority low priority critical priority high 我需要以这种方式排序 1 关键 2 高和3 低 如何才能做到这一点 我建议使用一个对象来存储排
  • contenteditable 在 safari 中不起作用,但在 chrome 中起作用

    我有一个奇怪的问题 这在 chrome 中按预期工作 但在 safari 中它只会发光 但不会对按键输入做出反应 这是触发文本版本的方法 var namebloc event currentTarget find column filena
  • 如何正确关闭 Node.js Express 服务器?

    我需要在收到回调后关闭服务器 auth github callback网址 与平常一样HTTP API http nodejs org docs latest api http html关闭 服务器目前支持server close call
  • 表单提交不起作用

    我有一张桌子 可以打印出所有可用的相机 它使用表单来更改这些设置 问题在于该表单仅更新条目中的最后一个摄像机 换句话说 如果我更改表单并为列表中的最后一个摄像机点击 应用 它将起作用 如果我更改此列表中任何其他摄像机的表单 它会将其更改为与
  • 检测 JavaScript 中的焦点丢失

    我希望能够检测 JavaScript 中任意元素何时失去焦点 因此我可以构建一个类似于 jEdit 的内联编辑工具 我不能依赖 jQuery 来实现这个库 所以我需要一个本机方法来完成它 我查看了 onblur 这似乎是正确的事情 但 MD
  • 如何混淆或使 JavaScript 文件不可读?

    我的应用程序中有 JavaScript 脚本 其中包含 JavaScript 和 jQuery 函数 所有用户与我的应用程序的交互都是动态的 并且通过 jQuery 传递到应用程序 我意识到 当我在客户端运行我的应用程序时 客户端可以通过查
  • jQuery 面板滑块通过单击按钮打开但不会关闭

    我的页面上有一个按钮 可以使用 jquery 和 Modernizr 框架打开右侧面板 按钮位于屏幕最右侧 单击时 它会向左滑动并打开打开的面板 问题是 再次单击时它不会滑回到原来的位置 HTML div class cd panel fr
  • 如何从普通 JavaScript 中的输入获取对象

    例如 我有 3 个输入
  • 当元素具有多个类时如何在 switch 语句中检查 className

    在下面的示例中 我只想单击该选项以在警报中显示 我正在尝试使用 switch 语句来确定单击了哪个类 如果我的 div 不包含多个类 则我的示例将有效 我尝试使用classList contains在我的 switch 语句中无济于事 有没
  • 是否可以从 webpack 中的文件名中删除特殊字符?

    长话短说 我的资产文件名中不能包含某些字符 例如连字符 我没有运气通过解析 webpack 文档来弄清楚是否可以使用正则表达式或类似的东西重命名文件 这样我就可以从我无法控制源文件名的 3rd 方包中删除任何连字符 我的超级天真的例子是这样
  • Firestore != 查询错误:“”!=”类型的参数无法分配给“WhereFilterOp”类型的参数。ts(2345)

    我的打字稿编译器有问题 此查询出现错误 const xxx admin firestore collection xxx where end timestampDate where end lt timestampDate get 错误 类
  • 当 jQuery .remove() 用于删除脚本标签时,它是否会清除加载的 JavaScript?

    正如标题所示 如果我使用以下命令从 DOM 中删除脚本标签 scriptid remove javascript 本身是保留在内存中还是被清除了 或者 我完全误解了浏览器处理 javascript 的方式吗 这是很有可能的 对于那些对我提问

随机推荐

  • 3、初识程序

    数据结构静态的描述了数据元素之间的关系 高效的程序需要在数据结构的基础上设计和选择算法 高效的程序 恰当的数据结构 合适的算法 算法是特定问题求解步骤的描述 在计算机中表现为指令的有限序列 算法是独立存在的一种解决问题的方法和思想 特性 输
  • 如何开始学习大数据

    最近很多人都想学习大数据开发 但是却不知道如何开始学习 传统的web应用 LAMP JavaEE NODE系等 与大数据什么关系 推荐一个大数据学习群 142973723每天晚上20 10都有一节 免费的 大数据直播课程 专注大数据分析方法
  • 荣耀社招笔试

    荣耀社招笔试题纪录篇 原文链接荣耀社招笔试题 十一放假回家参加了荣耀社招笔试 两道算法题 解析仅供参考 第一题 旋转矩阵 题目描述 给你一幅由 N N 矩阵表示的图像 其中每个像素的大小为 4 字节 请 你设计一种算法 将图像旋转 90 度
  • Linux查看已安装软件版本

    在Linux中 可以使用以下命令来查看软件版本 1 使用命令 软件名 version 来查看软件版本 例如 gcc version 2 使用命令 软件名 v 来查看软件版本 例如 python v 3 使用命令 rpm q 软件名 来查看已
  • idea windows找不到文件chrome

    一 原因分析 浏览器安装的地址有变动 二 解决 1 打开IDEA的设置页面 2 选中setting 找到tools目录下的Web Browsers 3 在电脑桌面上找到谷歌的图标 显示位置不一样 所以根据自身电脑找到谷歌图标 右键单击 gt
  • Generative Modeling by Estimating Gradients of the Data Distribution阅读笔记

    目录 概述 传统score based generative modeling介绍 score matching Langevin dynamics 传统score based generative modeling存在的问题 流形假设上的
  • Python的re.rearch()和group()详解, 及它们的综合使用

    re search 字符串1 字符串2 flags 0 以列表形式 返回在字符串2中所有匹配到的第一个字符串1 如果无 则返回空列表 其中flag参数一般很少使用 补充 re seaech re findall re match 等方法都有
  • 发送arp数据包

    代码来自busybox 函数询问TEST IP的mac地址 read interface函数通过ioctl来获取接口interface相关信息 arpping用于发送arp数据包 也可以使用s socket PF PACKET SOCK R
  • Java中\t的作用

    t是补全当前字符串长度到8的整数倍 最少1个最多8个空格 补多少要看你 t前字符串长度 比如当前字符串长度10 那么 t后长度是16 也就是补6个空格 如果当前字符串长度12 此时 t后长度是16 补4个空格
  • 19 个 K8S 日常故障处理集锦

    问题1 K8S集群服务访问失败 原因分析 证书不能被识别 其原因为 自定义证书 过期等 解决方法 更新证书即可 问题2 K8S集群服务访问失败 curl 7 Failed connect to 10 103 22 158 3000 Conn
  • 【Metashape精品教程15】点云分类 分类地面点 创建DEM

    Metashape精品教程15 点云分类 分类地面点 创建DEM 文章目录 Metashape精品教程15 点云分类 分类地面点 创建DEM 前言 一 点云分类 Classify Points 二 分类地面点 三 手动分类 四 创建DEM
  • 2023网络安全面试题汇总(附答题解析+配套资料)

    随着国家政策的扶持 网络安全行业也越来越为大众所熟知 相应的想要进入到网络安全行业的人也越来越多 为了更好地进行工作 除了学好网络安全知识外 还要应对企业的面试 所以在这里我归总了一些网络安全方面的常见面试题 希望对大家有所帮助 内容来自于
  • 标定协议之CCP协议基础知识介绍

    上一篇 标定协议基础知识介绍 中对标定协议进行了初步的介绍 从这一篇文章开始对CCP标定协议进行相关介绍 本篇将对CCP标定协议相关指令进行介绍 CCP通讯报文定义 CCP标定协议标准中定义了两条CAN通讯报文 CRO Command Re
  • Python 更新pip报错

    解决办法 更新指令中加上 user python m pip install user upgrade pip
  • Tomcat线程模型及调优

    一 Tomcat线程模型 1 BIO 同步阻塞式I O操作 表示Tomcat使用的是传统Java I O操作 即Java io包及其子包 Tomcat7以下版本默认情况下是以bio模式运行的 由于每个请求都要创建一个线程来处理 线程开销较大
  • case when where 显示没有该列 mysql,mysql - MySQL CASE WHEN麻木IS NULL忽略记录WHERE麻木IS NOT NULL - SO中文参考 - www.s...

    计算numb m value NULL 1 7 64 NULL 1 7 65 8070 2 7 935 8070 2 7 941 NULL 3 7 62 8070 4 7 92 8070 4 7 935的每个值的最小值和最大值 取决于COA
  • spring赌上未来的一击:WebFlux性能实测

    最近花了一点时间系统的测试验证了在SpringBoot框架下使用SpringMVC和Spring WebFlux两种框架开发接口 对比了响应时间以及压测吞吐量的区别 WebFlux SpringMVC 如果对WebFlux还不了解的同学 那
  • tesseract64位编译

    经过两天吐血的编译 tesseract终于可以在win64下用了 我这将每一步更加细化 我编译的是tesseract ocr3 02 leptonica1 68 1 要想编译自己的tesseract的lib和dll必须先编译leptonic
  • Linux中通过命令行监控股票报价

    如果你是那些股票投资者或者交易者中的一员 那么监控证券市场将是你的日常工作之一 最有可能的是你会使用一个在线交易平台 这个平台有着一些漂亮的实时图表和全部种类的高级股票分析和交易工具 虽然这种复杂的市场研究工具是任何严肃的证券投资者了解市场
  • 【Vue2.0源码学习】生命周期篇-初始化阶段(initEvents)

    文章目录 1 前言 2 解析事件 3 initEvents函数分析 4 总结 1 前言 本篇文章介绍生命周期初始化阶段所调用的第二个初始化函数 initEvents 从函数名字上来看 这个初始化函数是初始化实例的事件系统 我们知道 在Vue