当前位置: 首页 > news >正文

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

文章目录

    • 1. 前言
    • 2. 解析事件
    • 3. initEvents函数分析
    • 4. 总结

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.attrsListlet i, l, name, value, modifiersfor (i = 0, l = list.length; i < l; i++) {name  = list[i].namevalue = list[i].valueif (dirRE.test(name)) {// 解析修饰符modifiers = parseModifiers(name)if (modifiers) {name = name.replace(modifierRE, '')}if (onRE.test(name)) { // v-onname = 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.capturename = '!' + name // 给事件名前加'!'用以标记capture修饰符}// 判断是否有once修饰符if (modifiers.once) {delete modifiers.oncename = '~' + name // 给事件名前加'~'用以标记once修饰符}// 判断是否有passive修饰符if (modifiers.passive) {delete modifiers.passivename = '&' + name // 给事件名前加'&'用以标记passive修饰符}let eventsif (modifiers.native) {delete modifiers.nativeevents = 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.ondata.on = data.nativeOn// ...const name = Ctor.options.name || tagconst 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 eventsconst listeners = vm.$options._parentListenersif (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 = vmupdateListeners(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, eventfor (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 = curon[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.fnsif (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 handlersreturn fns.apply(null, arguments)}}invoker.fns = fnsreturn invoker
}

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

if (cur !== old) {old.fns = curon[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) : nameconst once = name.charAt(0) === '~'name = once ? name.slice(1) : nameconst capture = name.charAt(0) === '!'name = capture ? name.slice(1) : namereturn {name,once,capture,passive}
})

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

4. 总结

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

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

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

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

相关文章:

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

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

SQL高级知识点

MySQL基础 1、安装 1)设置编码 2)设置密码 2、配置文件&#xff1a;my.ini、my.cnf 1)设置端口号 port3306 2)设置编码 default-character-setutf8character-set-serverutf8 3)存储引擎 default-storage-engineINNODB 4)最大连接数 max_connections100 注意&…...

【安全】原型链污染 - Code-Breaking 2018 Thejs

目录 准备工作 环境搭建 加载项目 复现 代码审计 payload 总结 准备工作 环境搭建 Nodejs BurpSuite 加载项目 项目链接 ① 下载好了cmd切进去 ② 安装这个项目 可以检查一下 ③运行并监听 可以看到已经在3000端口启动了 复现 代码审计 const fs require(fs) cons…...

【架构】探索计算机处理器的世界:ARM和x86架构解析及指令集

目录 导语ARM架构x86架构AMD公司对比与应用不同架构处理器的指令集结语 导语 计算机处理器是数字化时代的核心引擎&#xff0c;而在众多处理器架构中&#xff0c;ARM和x86是备受关注的三个。本文将带您深入探索这三个架构&#xff0c;介绍它们的特点、公司背景以及应用领域。让…...

SpringBoot权限认证

SpringBoot的安全 常用框架&#xff1a;Shrio,SpringSecurity 两个功能&#xff1a; Authentication 认证Authorization 授权 权限&#xff1a; 功能权限访问权限菜单权限 原来用拦截器、过滤器来做&#xff0c;代码较多。现在用框架。 SpringSecurity 只要引入就可以使…...

OpenGL-入门-BMP像素图glReadPixels

glReadPixels函数用于从帧缓冲区中读取像素数据。它可以用来获取屏幕上特定位置的像素颜色值或者获取一块区域内的像素数据。下面是该函数的基本语法&#xff1a; void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *da…...

同源策略以及SpringBoot的常见跨域配置

先说明一个坑。在跨域的情况下&#xff0c;浏览器针对复杂请求&#xff0c;会发起预检OPTIONS请求。如果服务端对OPTIONS进行拦截&#xff0c;并返回非200的http状态码。浏览器一律提示为cors error。 一、了解跨域 1.1 同源策略 浏览器的同源策略&#xff08;Same-Origin Po…...

基于jeecg-boot的flowable流程跳转功能实现

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 今天我…...

react图片预加载

道阻且长&#xff0c;行而不辍&#xff0c;未来可期 图片预加载的原理&#xff1a;new一个image对象&#xff0c;用这个对象加载图片&#xff0c;等这个对象将这个图片请求完后&#xff0c;再将这个图片放入原本应该放置的位置 代码如下&#xff1a; import React, { useEffe…...

数据库管理

SQL语言分类&#xff1a; DDL&#xff1a;数据定义语言&#xff0c;用于创建数据库对象&#xff0c;如库、表、索引等 DML&#xff1a;数据操纵语言&#xff0c;用于对表中的数据进行管理 DQL&#xff1a;数据查询语言&#xff0c;用于从数据表中查找符合条件的数据记录 DCL&am…...

【2023年11月第四版教材】《第8章-整合管理》(第3部分)

《第8章-整合管理》&#xff08;第3部分&#xff09; 9 监控项目工作9.1 监控项目工作★★★9.2 数据分析9.4 决策9.5 工作绩效报告 10 实施整体变更控制10.1 实施整体变更控制★★★ &#xff08;18上36&#xff09;10.2 变更请求★★★10.3变更控制工具★★★10.4 数据分析10…...

初阶数据结构(三)链表

&#x1f493;博主csdn个人主页&#xff1a;小小unicorn&#x1f493; ⏩专栏分类&#xff1a;c &#x1f69a;代码仓库&#xff1a;小小unicorn的学习足迹&#x1f69a; &#x1f339;&#x1f339;&#x1f339;关注我带你学习编程知识 前面我们讲的线性表的顺序存储结构。它…...

Python小知识 - 八大排序算法

八大排序算法 排序算法是计算机科学中非常重要的一个研究领域。排序算法可以分为内部排序和外部排序&#xff0c;内部排序是数据记录在计算机内部&#xff0c;而外部排序是数据记录在计算机外部&#xff0c;这里我们主要讨论内部排序。 内部排序中的算法大致可以归纳为四类&…...

安卓动态申请权限

我们在使用一些官方app时&#xff0c;刚下载进去之后经常会弹出各种各样的权限获取请求&#xff0c;今天简单学习了下&#xff0c;希望不会误人子弟哈哈哈哈。 一、将需要用到的权限添加到Manifest清单里 <uses-permission android:name"android.permission.WRITE_EXT…...

关于亚马逊云科技云技能孵化营学习心得

1、活动介绍 本活动主要是面向想要全面了解亚马逊云科技 (Amazon Web Services) 云的个人&#xff0c;而不受特定技术角色的限制。内容包括亚马逊云科技云概念、亚马逊云科技服务、安全性、架构、定价和支持等等&#xff0c;此外还可以参加亚马逊的认证考试。 2、学习过程 该…...

计算机安全学习笔记(III):强制访问控制 - MAC

基本概念 强制访问控制是一种高级访问控制机制&#xff0c;旨在通过强制执行事先定义的安全策略&#xff0c;实现资源和信息的严格保护。与自主访问控制&#xff08;Discretionary Access Control&#xff0c;DAC&#xff09;不同&#xff0c;MAC 的控制权不由用户自身决定&am…...

java判断ip是否为指定网段

具体网络知识原理请看这个博文 /**** param address servletRequest.getRemoteAddr();* param host servletRequest.getRemoteHost();* return* Description 检验IP是否符合安全限定*/private boolean ipIsInNet(String address, String host){Set<String> iPset allow…...

如何通过人工智能和自动化提高供应链弹性?

全球供应链中的数字化转型已经引起了广泛关注&#xff0c;尽管在过去的十年中&#xff0c;这一话题被广泛讨论&#xff0c;但许多公司仍然对如何实现这一不明确的目标感到困惑。人们普遍认识到这种转变的重要性&#xff0c;而新冠疫情及其带来的巨大影响也为行业向数字化转型方…...

【Apollo学习笔记】——规划模块TASK之PATH_REUSE_DECIDER

文章目录 前言PATH_REUSE_DECIDER功能简介PATH_REUSE_DECIDER相关配置PATH_REUSE_DECIDER总体流程PATH_REUSE_DECIDER相关子函数IsCollisionFreeTrimHistoryPathIsIgnoredBlockingObstacle和GetBlockingObstacleS Else参考 前言 在Apollo星火计划学习笔记——Apollo路径规划算…...

框架分析(6)-Ruby on Rails

框架分析&#xff08;6&#xff09;-Ruby on Rails 专栏介绍Ruby on Rails核心概念以及组件讲解MVC架构模式约定优于配置强大的ORM支持自动化测试丰富的插件生态系统RESTful路由安全性总结 优缺点优点快速开发简单易学MVC架构强大的ORM支持大量的插件和Gem支持 缺点性能问题学习…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

MMaDA: Multimodal Large Diffusion Language Models

CODE &#xff1a; https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA&#xff0c;它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构&#xf…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表

##鸿蒙核心技术##运动开发##Sensor Service Kit&#xff08;传感器服务&#xff09;# 前言 在运动类应用中&#xff0c;运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据&#xff0c;如配速、距离、卡路里消耗等&#xff0c;用户可以更清晰…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)

前言&#xff1a; 最近在做行为检测相关的模型&#xff0c;用的是时空图卷积网络&#xff08;STGCN&#xff09;&#xff0c;但原有kinetic-400数据集数据质量较低&#xff0c;需要进行细粒度的标注&#xff0c;同时粗略搜了下已有开源工具基本都集中于图像分割这块&#xff0c…...

JVM 内存结构 详解

内存结构 运行时数据区&#xff1a; Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器&#xff1a; ​ 线程私有&#xff0c;程序控制流的指示器&#xff0c;分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 ​ 每个线程都有一个程序计数…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)

本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...

android RelativeLayout布局

<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...