【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对象里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/60943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AUTOSAR开发工具DaVinci Configurator里的Modules

DaVinci Configurator 里面有个Module这个概念。 如你所想&#xff0c;基本上跟AUTOSAR架构里面的Module相对应 从软件的Project菜单中的Basic Editor项可以打开 打开这个菜单后&#xff0c;会看到很多Modules项以及其相关配置项 这个Basic Editor显示出整个ECU配置中的所有…

代码题: 看代码说结果, 事件循环 + async 函数的

1. 基本的 async/await 和事件循环 console.log(1);async function asyncFunc() {console.log(2);await Promise.resolve();console.log(3); }asyncFunc();console.log(4);执行顺序&#xff1a; 打印 1定义异步函数 asyncFunc&#xff0c;但并不执行它。调用 asyncFunc()。 打…

Python中的迭代器与生成器

文章目录 1、迭代器2、生成器3、列表推导式和生成器表达式4、enumerate() 在Python中&#xff0c;迭代器&#xff08;Iterator&#xff09;和生成器&#xff08;Generator&#xff09;是两种用于处理可迭代对象的重要工具。而可迭代对象包括列表&#xff0c;元组&#xff0c;字…

C#里Bitmap转Halocn的HObject

一般情况下&#xff0c;图像的width是4的倍数的话&#xff0c;用以下代码便可将彩色bitmap转出halcon里的HObject public void Bitmap2HObject(Bitmap bmp, out HObject image){try{Rectangle rect new Rectangle(0, 0, bmp.Width, bmp.Height);BitmapData srcBmpData bmp.L…

day-06 多进程服务器端 -- 进程间通信

一.多进程服务器端 &#xff08;一&#xff09;进程概念及应用 利用之前学习到的内容&#xff0c;我们的服务器可以按照顺序处理多个客户端的服务请求。在客户端和服务时间增长的情况下&#xff0c;服务器就不足以满足需求了。 1.两种类型的服务器端 &#xff08;1&#xff…

记录--解决前端内存泄漏:问题概览与实用解决方案

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 内存泄漏是前端开发中的一个常见问题&#xff0c;可能导致项目变得缓慢、不稳定甚至崩溃。在本文中&#xff0c;我们将深入探讨在JavaScript、Vue和React项目中可能导致内存泄漏的情况&#xff0c;并提…

xml和json互转工具类

分享一个json与xml互转的工具类&#xff0c;非常好用 一、maven依赖 <!-->json 和 xm 互转</!--><dependency><groupId>org.dom4j</groupId><artifactId>dom4j</artifactId><version>2.1.3</version></dependency&g…

使用kafka还在依赖Zookeeper,kraft模式了解下

Kafka的Kraft模式 概述 ​ Kafka是一种高吞吐量的分布式发布订阅消息系统&#xff0c;它可以处理消费者在网站中的所有动作流数据。其核心组件包含Producer、Broker、Consumer&#xff0c;以及依赖的Zookeeper集群。其中Zookeeper集群是Kafka用来负责集群元数据的管理、控制器…

如何查看数据集下载后保存的绝对路径?

1.问题 当我们下载torchvision.datasets里面的数据集时&#xff0c;有时候会遇到找不到数据集保存路径的问题。 2.解决 引入os库 import os调用如下方法 os.path.abspath(数据集对象.root)以下面代码为例 import os import torchvision.datasets as datasets# 指定数据集…

vue3+tsx+element-plus中遇到的一些问题总结

前言&#xff1a; vue3tsxelement-plus中遇到的一些问题总结 1、手动打开/关闭 el-tooltip 1、定义ref const tooltipMoreRef ref(); 2、在标签上加ref <el-tooltip ref{tooltipMoreRef} content{t("chat.messageBox.more")} placement"top" effect&…

Java篇(输入输出和快捷键)

目录 定义 输入&#xff1a; 使用 Scanner类读入 循环读入数据 输出&#xff1a; 1.println() 2.println () 3.printf() 4.格式化输出 快捷输入 定义 1.输入&#xff08;Input&#xff09;输出&#xff08;Output&#xff09;—— IO 流 输入流&#xff1a;以电脑为…

Java连接websocket优雅断线、重连功能

为了实现优雅重连和重试&#xff0c;您需要在代码中添加一些逻辑来处理连接失败或断开连接的情况。 实现代码如下&#xff1a; import javax.websocket.*; import java.io.IOException;ClientEndpoint public class WebSocketClientEndpoint {private Session userSession n…

算法训练营第四十天(8.31)| 动态规划Part10:购买股票

目录 Leecode 309.买卖股票的最佳时机含冷冻期 Leecode 714.买卖股票的最佳时机含手续费 Leecode 309.买卖股票的最佳时机含冷冻期 题目地址&#xff1a;​​​​​​​力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目类型&#xff1a;股票问题 c…

设备报修系统有什么用?企业如何提高维修效率和质量?

在数字化时代&#xff0c;基于人工智能和大数据技术的设备报修系统已经成为企业提高服务质量和效率的重要手段。这种系统可以为用户提供方便快捷的报修方式&#xff0c;例如通过扫描设备上的二维码或通过公众号、企业微信、钉钉等平台提交报修请求。这种报修系统不仅可以提高故…

学习笔记:ROS使用经验(rviz)

rviz_config.rviz里面的参数是什么结构 在RViz中的配置文件&#xff08;例如rviz_config.rviz&#xff09;使用YAML格式来定义不同的可视化参数和设置。以下是一些常见的参数结构&#xff1a; Class: 指定显示或组件的类别&#xff0c;如rviz/PointCloud2、rviz/MarkerArray、…

香港服务器快还是台湾服务器快?

​  基于机房位置不同&#xff0c;香港服务器相对于台湾服务器在访问速度方面有一定的优势。香港服务器拥有CN2线路&#xff0c;因此访问速度较快。在网络服务商方面&#xff0c;中华电信等台湾服务商提供的带宽也具有很高的性价比。 香港服务器对大陆用户的影响 对于大陆用户…

Springboot - 3.容器集成

Web容器 当选择Web容器时&#xff0c;你可以在Spring Boot应用中集成不同的容器。下面是Undertow、Tomcat、Jetty、Netty、WebLogic、WebSphere和WildFly这些常见Web容器的比较&#xff0c;包括它们的特点和优缺点&#xff1a; 容器特点优点缺点Undertow轻量级、嵌入式支持、…

详解html中的doctype

选择什么样的DOCTYPE XHTML 1.0中有3种DTD&#xff08;文档类型定义&#xff09;声明可以选择&#xff1a;过渡的&#xff08;Transitional&#xff09;、严格的&#xff08;Strict&#xff09;和框架的&#xff08;Frameset&#xff09;。这里分别介绍如下。 1&#xff0e…

如何增长LLM推理token,从直觉到数学

背景&#xff1a; 最近大模型输入上文长度增长技术点的研究很火。为何要增长token长度,为何大家如此热衷于增长输入token的长度呢&#xff1f;其实你如果是大模型比价频繁的使用者&#xff0c;这个问题应该不难回答。增长了输入token的长度&#xff0c;那需要多次出入才能得到…

软考:中级软件设计师:信息系统的安全属性,对称加密和非对称加密,信息摘要,数字签名技术,数字信封与PGP

软考&#xff1a;中级软件设计师:信息系统的安全属性 提示&#xff1a;系列被面试官问的问题&#xff0c;我自己当时不会&#xff0c;所以下来自己复盘一下&#xff0c;认真学习和总结&#xff0c;以应对未来更多的可能性 关于互联网大厂的笔试面试&#xff0c;都是需要细心准…