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

17_事件的处理

目录

    • 绑定事件与解绑事件
    • 优化事件的绑定和解绑方式
    • 处理不同事件类型的绑定
    • 处理同一事件类型多个事件处理函数
    • 事件冒泡与更新时机问题

绑定事件与解绑事件

既然要处理事件,那么首先面临的问题是如何在 vnode 中描述这个事件,在 vnode.props 中,凡是以 on 开头的字符串,都被视作事件,例如:

const vnode = {type: 'h1',props: {onClick: () => {alert('world')}},children: 'hello'
}

当有了正确的描述之后,我们需要做的就是在处理 vnode.props 的时候,增加一个分支,当检测到以 on 开头的属性之后,就作为事件处理,如下:

export function patchProp(el, key, prevValue, nextValue) {if (key === 'class') {patchClass(el, nextValue)} else if (key === 'style') {patchStyle(el, prevValue, nextValue)} else if (isOn(key)) {patchEvent(el, key, prevValue, nextValue)}// 处理 DOM Propertieselse if (shouldSetAsProp(el, key)) {patchDOMProp(el, key, nextValue)}// 处理 HTML Attributeselse {patchAttr(el, key, nextValue)}
}// isOn 方法实现非常简单,如下:
function isOn(value) {return value.startsWith('on')
}

忘记这块处理逻辑的可以翻阅之前的文档,patchEvent 具体实现如下:

export function patchEvent(el, key, prevValue, nextValue) {// 获取事件名称//  - onClick -> clickconst evnetName = key.slice(2).toLowerCase()// 根据绑定的事件进行处理if (nextValue) {// 绑定事件el.addEventListener(evnetName, nextValue)}
}

那我们来看一下这个是否可行呢,如图:

在这里插入图片描述

解决初次绑定,那么如果是更新事件应该如何处理呢?我们很容易想到的方式就是,直接将上一次绑定的事件移除,如下:

export function patchEvent(el, key, prevValue, nextValue) {const evnetName = key.slice(2).toLowerCase()// 移除prevValue && el.removeEventListener(evnetName, prevValue)if (nextValue) {el.addEventListener(evnetName, nextValue)}
}

这样做,代码确实能够按照预期的进行工作,但是不够好,我们可以使用一种性能更优的方式来解决。

优化事件的绑定和解绑方式

在绑定事件时,我们可以绑定一个伪造的事件处理函数 invoker,然后把真正的事件处理函数设置为 invoker.value 属性的值,这样当更新事件的时候,我们将不再需要调用 removeEventListener 函数来移除上一次的事件,只需要更新 invoker.value 的值即可,如下:

export function patchEvent(el, key, prevValue, nextValue) {const evnetName = key.slice(2).toLowerCase()// 获取上一次的伪造的事件处理函数 invokerlet invoker = el._veiif (nextValue) {// 如果存在新的vnode且且上一次的invoker不存在,则创建新的invokerif (!invoker) {// vei 是 vue event invoker 的缩写invoker = el._vei = e => {// invoker.value 的值就是真正要执行的事件回调函数invoker.value(e)}// 把真正的事件处理函数赋值给 invoker.valueinvoker.value = nextValue// 绑定 invoker 为事件处理函数el.addEventListener(evnetName, invoker)}// 如果新的vnode存在且invoker也存在,则表示这是一次更新操作else {// 此时:我们的更新操作要做的就只是把 invoker.value 更新成新的回调函数即可//  - 而不再需要重新绑定事件处理函数invoker.value = nextValue}} else if (invoker) {// 如果新的 vnode 不存在,且 invoker 存在,则表示这个 el 之前绑定过事件函数,需要解绑el.removeEventListener(evnetName, invoker)}
}

此时性能就得到了优化,但是 invoker 的作用还不至于此,还可以解决事件冒泡和事件更新之间的相互影响的问题,不过此时我们先暂时不探讨这个问题。

处理不同事件类型的绑定

我们目前面临的是一个其他的问题,我们现在是把事件处理函数缓存到 el._vei 属性中,问题是,在同一时刻只能缓存一个事件处理函数,这意味这如果绑定了多种事件,就会出现事件覆盖的问题,比如同事存在 click 和 mousedown 事件,如下:

const vnode = {type: 'h1',props: {onClick: () => {console.log('我是 click 事件')},onMouseDown: () => {console.log('我是 mousedown 事件')}},children: 'hello'
}

我们现在绑定了两个事件,来看一下打印的结果,如图:

在这里插入图片描述

就仅仅输出了一个,并且是后者覆盖前者,所以为了处理不同类型的事件时,我们需要修改一下缓存事件的结构,如下:

export function patchEvent(el, key, prevValue, nextValue) {const evnetName = key.slice(2).toLowerCase()// 将 el._vei 改为对象let invokers = el._vei || (el._vei = {})// 根据事件名称获取对应的invokerlet invoker = invokers[key]if (nextValue) {if (!invoker) {// 进行缓存的时候,通过 key 作为区分,避免覆盖invoker = el._vei[key] = e => {invoker.value(e)}invoker.value = nextValueel.addEventListener(evnetName, invoker)} else {invoker.value = nextValue}} else if (invoker) {el.removeEventListener(evnetName, invoker)}
}

现在我们来看一下执行的结果,如图:

在这里插入图片描述

处理同一事件类型多个事件处理函数

当完成了不同类型的事件绑定之后,我们还面临一个问题,就是同一个事件类型的事件绑定多个事件处理函数时,只会执行一次,vnode 如下:

const vnode = {type: 'h1',props: {onClick: [() => {console.log('我是 click1事件')},() => {console.log('我是 click2 事件')}],},children: 'hello'
}

按照预期是两个都会触发,但是实际情况并不是这样,所以为了达到这个目标,我们还需要调整一下代码,如下:

export function patchEvent(el, key, prevValue, nextValue) {const evnetName = key.slice(2).toLowerCase()let invokers = el._vei || (el._vei = {})let invoker = invokers[key]if (nextValue) {if (!invoker) {invoker = el._vei[key] = e => {// 如果 invoker.value 是一个数组,则依次执行if (isArray(invoker.value)) {invoker.value.forEach(fn => fn(e))}// 不是数组,则表示只有一个事件处理函数,直接执行else {invoker.value(e)}}// nextValue 可能会直接是一个函数,也可能会是一个包含多个函数的数组invoker.value = nextValueel.addEventListener(evnetName, invoker)} else {invoker.value = nextValue}} else if (invoker) {el.removeEventListener(evnetName, invoker)}
}

执行结果如图:

事件冒泡与更新时机问题

我们来看一下下面这个案例,如下:

const bol = ref(false)effect(() => {console.log('effect')const vnode = {type: 'div',props: bol.value? {onClick() {console.log('父元素 clicked')}}: {},children: [{type: 'p',props: {onClick() {bol.value = trueconsole.log('子元素 p clicked', bol.value)}},children: 'click me'}]}render(vnode, document.querySelector('#app'))
})

根据这段代码,我们可以思考一个问题,首次渲染完成之后,点击 p 标签的点击事件是否会触发 div 的点击事件?

根据代码的逻辑分析,一开始 bol 的值为 false,则不会给 div 绑定点击事件,那么点击 p 元素时,就算因为事件冒泡的存在,但是因为没有给 bol 绑定点击事件,所以就不会触发 div 的点击事件。

但是实际上却不是,在运行这段代码时,触发了 p 元素的点击事件也会触发 div 的点击事件,这是因为在 p 元素的点击事件中奖 bol 的值改为 true,我们又处于 effect 函数中,bol 是一个响应式数据,所以 bol 发生改变后就会立即执行 effect 函数,自然也会重新执行 render 函数渲染,也就是说在 p 元素的 click 点击事件还没处理完成的时候,就会再次渲染了,再次渲染就会给 div 绑定点击事件,而当 p 元素点击事件执行完成之后,进行冒泡,此时 div 已经绑定了点击事件,所以就会触发 div 的点击事件。

根据这个流程,我们可以使用一个图来表达执行顺序,如图:
在这里插入图片描述

那如何解决呢,我们其实在这个图中可以发现,事件触发的时间是在时间处理函数被绑定之前的,这就意味着事件触发时,目标元素上还没有绑定相关事件处理函数,我们可以根据这个特点来解决问题,即屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行,如下:

export function patchEvent(el, key, prevValue, nextValue) {const evnetName = key.slice(2).toLowerCase()let invokers = el._vei || (el._vei = {})let invoker = invokers[key]if (nextValue) {if (!invoker) {invoker = el._vei[key] = e => {// e.timeStamp 是事件触发的时间// 如果 e.timeStamp 小于于绑定事件的时间,则不执行if (e.timeStamp < invoker.attrched) returnif (isArray(invoker.value)) {invoker.value.forEach(fn => fn(e))} else {invoker.value(e)}}invoker.value = nextValue// 添加时间绑定的时间invoker.attrched = performance.now()el.addEventListener(evnetName, invoker)} else {invoker.value = nextValue}} else if (invoker) {el.removeEventListener(evnetName, invoker)}
}

这样就可以解决了,为什么可以解决呢,我们就来分析一下这新增的两行代码,performance.now() 是一个高精时间,具体的可以查阅文档DOMHighResTimeStamp,这时间源是创建浏览器上下文的时间,在这里可以简单理解为一个只会增加的时间,页面刷新就会重置重新计算,所以首次渲染之后,点击 p 元素,触发点击事件,将 bol 的值改为 true,此时会执行 effect 然后绑定事件,并记录下绑定的时间,重点来了,此时的 e.timeStamp 触发的时间是 p 事件触发的时间,而非这个冒泡到 div 的事件触发时间,所以这个 timeStamp 一定比 div 的事件绑定时间要早,而比这个早就会被 if (e.timeStamp < invoker.attrched) return 这句代码拦截,而后续触发的时候,invoker 已经存在,所以不会重新设置 invoker.attrched 的值,所以后续触发时,e.timeStamp 的时间就是大于 invoker.attrched 所记录的时间。这样就解决了我们的问题。

如果不够直观,我们可以看一下初次渲染点击的时间记录,如图:

在这里插入图片描述

再来看看再次点击的执行结果,如图:

在这里插入图片描述

相关文章:

17_事件的处理

目录 绑定事件与解绑事件优化事件的绑定和解绑方式处理不同事件类型的绑定处理同一事件类型多个事件处理函数事件冒泡与更新时机问题 绑定事件与解绑事件 既然要处理事件&#xff0c;那么首先面临的问题是如何在 vnode 中描述这个事件&#xff0c;在 vnode.props 中&#xff0…...

1FreeRTOS学习(队列、二值信号量、计数型信号量之间的相同点和不同点)

相同点&#xff1a; &#xff08;1&#xff09;传递区间 队列、二值信号量、计数型信号量均可用在任务与任务&#xff0c;任务与中断之间进行消息传递 &#xff08;2&#xff09; 传递方式 创建队列--发送队列--接受队列 创建二值信号量--发送二值信号量--接受二值信号量 创建计…...

数据库设计与范式及其应用

数据库设计是数据库管理系统&#xff08;DBMS&#xff09;中的核心环节&#xff0c;良好的数据库设计不仅可以提高数据存取的效率&#xff0c;还能增强数据的可维护性和一致性。范式&#xff08;Normalization&#xff09;是一种设计原则&#xff0c;用于减少数据冗余和提高数据…...

笔记-配置PyTorch(CUDA 12.2)

文章目录 前言一、安装 PyTorch&#xff08;CUDA 12.2&#xff09;1. 创建并激活 Conda 环境2. 安装 PyTorch&#xff08;CUDA 12.2&#xff09;3. 安装 torch_geometric 及依赖项4. 验证安装 总结 前言 一、安装 PyTorch&#xff08;CUDA 12.2&#xff09; 1. 创建并激活 Con…...

[C++]——红黑树(附源码)

目录 一、前言 二、正文 2.1 红黑树的概念 2.2 红黑树的性质 2.3红黑树节点的定义 2.4 红黑树的插入 2.4.1 情况一 2.4.2 情况二 ​编辑 2.4.3 情况三 2.5 红黑树的验证 三、全部代码 四、结语 一、前言 在上一篇博客中&#xff0c;为小伙伴们进行了AVL树的讲解&#…...

网络文件系统搭建

在CentOS7上搭建网络文件系统&#xff08;NFS&#xff09;&#xff0c;并让客户端进行挂载&#xff0c;具体步骤如下&#xff1a; 1. 服务器端操作 安装NFS服务器软件包&#xff1a; 执行以下命令安装NFS服务&#xff1a; sudo yum install nfs-utils -y 启动并启用NFS服务&…...

基于vue、VantUI、django的程序设计

首先构建vue项目&#xff0c;构建项目点这里 安装 npm install axios axios简介 Axios 是一个基于 promise 的 HTTP 库&#xff0c;用于发起请求和接收响应&#xff0c;实现异步操作 基本使用 axios对象 请求响应拦截 在utils文件夹里新建ajax.js 创建一个axios对象并…...

京准电钟解读:NTP网络对时服务器助力厂区改造方案

京准电钟解读&#xff1a;NTP网络对时服务器助力厂区改造方案 京准电钟解读&#xff1a;NTP网络对时服务器助力厂区改造方案 1&#xff09;系统概述 时钟系统可通过网络进行管理及时间校对&#xff0c;为厂区提供高精度、全天时、全天候 的授时服务&#xff0c;统一全厂各种系统…...

本地docker-compose仓库搭建以及推送docker镜像到仓库

前言 以下部分知识只适用于linux&#xff0c;不适合小白&#xff0c;请自行甄别执行 1.搭建 #参考 https://blog.csdn.net/u011535199/article/details/107457275 version: 3 services:registry:restart: alwaysimage: registry:2ports:- 5000:5000environment:#REGISTRY_HT…...

WPF+MVVM案例实战(八)- 自定义开关控件封装实现

文章目录 1、案例运行效果2、项目准备2、功能实现1、控件模板实现2、控件封装1、目录与文件创建2、各文件功能实现 3、开关界面与主窗体菜单实现1、开关界面实现2、主窗体菜单实现 4、源代码获取 1、案例运行效果 2、项目准备 打开项目 Wpf_Examples&#xff0c;新建ToggleBut…...

单机kafka性能需要高性能的硬件做支撑

一般来说&#xff0c;单机kafka在硬件支持的情况下&#xff0c;能支持每秒100万写入&#xff0c;如果硬件没有那么好的话(机械硬盘&#xff0c;容器内给内存8G&#xff0c; CPU也不是很好)&#xff0c;就只能减少每秒的写入量&#xff0c;每秒写入5万都比较不错了。 如果强行每…...

Spark 的 Http Broadcast 和 Torrent Broadcast 广播实现类的对比

在 Apache Spark 中&#xff0c;广播机制用于高效地将小型只读数据分发到集群中的各个执行器&#xff08;Executor&#xff09;。Spark 中主要有两种不同的广播实现方式&#xff1a;Http Broadcast 和 Torrent Broadcast。这两种方式的核心目标都是将数据高效地分发给所有工作节…...

030_Subplot_In_Matlab中多图绘制之subplot函数

基于子图的多图方法 专业的论文中通常涉及到多个有逻辑关系的图拼接在一起&#xff0c;构成相互支持或者对照。所以很早之前&#xff0c;Matlab就有这个子图的函数subplot。 这个函数的基本语义有三类&#xff1a; 在图窗上划分出一个矩形区域建立一个坐标系&#xff0c;并指…...

免费云服务器有什么使用限制和注意事项?

在数字化时代&#xff0c;云计算已经成为许多企业和个人用户的重要工具。对于初创企业、开发者和学生来说&#xff0c;免费的云服务器提供了一个低成本的解决方案&#xff0c;使他们能够进行项目开发、学习和实验。但在使用过程中也存在一些限制和注意事项。以下是主要的使用限…...

3-ZYNQ 折腾记录 -PS_PL AXI Interfaces

Zynq UltraScale MPSoC集成了功能丰富的四核或双核Arm Cortex-A53 MPCore基于处理系统(Processing System, PS)和可编程逻辑(Programmable Logic, PL)的单一设备。 PS和PL可以使用多个接口和其他信号进行紧密或松散的耦合。这使设计人员能够有效地将用户创建的硬件加速器和其他…...

总结test

1.IO流 |-- 字节流操作任何类型文件|-- 字符流操作纯字符类文件|-- BIO 传统IO流&#xff0c;阻塞型的&#xff0c;也就是BIO&#xff0c;当执行IO流时&#xff0c;CPU只能等待执行完当前任务&#xff0c;才能去执行其他线程任务|-- NIO非阻塞型IO流&#xff0c;CPU可以同时执行…...

在 On hold 期刊 eLife 上发表一篇生信文章需要什么工作量?

生信碱移 科研圈动态 根据弗雷赛斯以及相关媒体最新消息&#xff0c;中科院一区TOP&#xff0c;著名生命科学期刊 eLife [IF: 6.4]已被科睿唯安官方 On hold&#xff01; ▲ 官网截图。图片来源&#xff1a;https://mjl.clarivate.com/home eLife是一本专注于生物医学和生命科…...

使用Django框架开发企业级Web应用

&#x1f496; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4bb; Gitee主页&#xff1a;瑕疵的gitee主页 &#x1f680; 文章专栏&#xff1a;《热点资讯》 使用Django框架开发企业级Web应用 1 引言 2 Django简介 3 安装Python与Django 4 创建Django项目 5 设计应用结构 6 创…...

认识线程 — JavaEE

目录 认识线程&#xff08;Thread&#xff09; 1 线程是什么? 2 为什么要有线程 3 进程和线程的区别 区别一 区别二 区别三 区别四 4. Java的线程和操作系统线程的关系 认识线程&#xff08;Thread&#xff09; 1 线程是什么? 一个线程就是一个 "执行流"。…...

【C++单调栈】853. 车队|1678

本文涉及的基础知识点 C单调栈 LeetCode853. 车队 在一条单行道上&#xff0c;有 n 辆车开往同一目的地。目的地是几英里以外的 target 。 给定两个整数数组 position 和 speed &#xff0c;长度都是 n &#xff0c;其中 position[i] 是第 i 辆车的位置&#xff0c; speed[i…...

第十届文荣奖华丽开幕,郁葱以青春与努力绽放青年演员光芒

10月27日&#xff0c;第十届文荣奖在众人的期待中盛大开启&#xff0c;内地青年女演员郁葱受邀出席&#xff0c;作为国内颇具影响力的影视奖项&#xff0c;文荣奖一直以来都致力于发掘和表彰优秀的影视作品和青年影视人才&#xff0c;为影视行业的发展注入新的活力&#xff0c;…...

CMake 生成器表达式介绍

【写在前面】 生成器表达式在构建系统生成期间进行评估&#xff0c;以生成特定于每个构建配置的信息。它们的形式为 $<...>。例如&#xff1a; target_include_directories(tgt PRIVATE /opt/include/$<CXX_COMPILER_ID>) 这将扩展为 “/opt/include/GNU”、“/opt…...

ubuntu 20.04编译驱动报gcc-12 not found错误

最近在自己安装的Ubuntu 系统上编译自定义驱动&#xff0c;发现无法编译.ko,错误如下&#xff1a; 按照如下操作&#xff0c;发现可以解决&#xff0c;记录下&#xff0c;主要是Ubuntu缺少g-12的包 安装包以后发现可以正常编译...

docker sameersbn/bind dns服务器

1. 安装 #下载docker 镜像 docker pull sameersbn/bind#运行 53端口若被占用会启动失败 docker run --name dns -d --restartalways \ --publish 53:53/tcp \ --publish 53:53/udp \ --publish 10000:10000/tcp \ -v /etc/localtime:/etc/localtime \ -v /data/bind/:/data \…...

错误:无法推送一些引用到 ‘https://gitee.com/chek_kk/python-electron-app.git‘

这个错误提示说明在提交时某个文件的大小超过了 Gitee 仓库的单文件大小限制&#xff08;100MB&#xff09;。你需要从Git 历史中彻底移除这个大文件&#xff0c;否则无法推送到远程仓库。 解决步骤 1. 确认大文件信息 使用以下命令找出超过限制的大文件&#xff1a; git re…...

深度剖析美区代理IP的多元应用与优势

在当今数字时代&#xff0c;代理IP&#xff08;Proxy IP&#xff09;已成为互联网使用中的一项关键技术。尤其在美区&#xff0c;代理IP在数据采集、网络安全及在线隐私保护等领域发挥着越来越重要的作用。本文将深入探讨代理IP的基本概念、应用场景以及它带来的诸多优势&#…...

基于KV260的基础视频链路通路(MIPI+Demosaic+VDMA)

目录 1. 简介 1.1 要点 1.2 背景 1.2.1 Got stuck 1.2.2 Cant be Initialized 2. Overlay 2.1 参考 Overlay 2.1.1 KV260 Base 2.1.2 Pynq-CV-OV5640 2.2 自建 Overlay 2.2.1 IIC IP 2.2.2 MIPI CSI-2 Rx 2.2.3 AXI4-S Subset 2.2.4 Demosaic 2.2.5 Pixel Pack …...

Uni-App-04

主页开发 保存主页数据 <script> import { indexData, base } from /serviceexport default {data() {return {base, //把服务器基础地址变量设置为数据属性carousels:[], //轮播广告条目列表menuItems:[], //当前用户选中的功能菜单列表activities:[], //最新的…...

ElasticSearch分片

本文内容参考了田雪松老师编著的《Elastic Stack应用宝典》 ElasticSearch作为一个搜索引擎&#xff0c;会存储海量的数据。而存储海量的数据&#xff0c;就要解决如何存储的问题&#xff0c;并且保证数据不会丢失&#xff0c;同时还需要保证数据检索的效率&#xff0c;尽可能…...

spring高手之路

以下是一些可以快速入门Spring的方法&#xff1a; 1. 学习基础知识 阅读官方文档&#xff1a;Spring官方文档是最权威的学习资料。它详细介绍了Spring的各个模块、概念和使用方法。从核心模块开始&#xff0c;了解如依赖注入&#xff08;DI&#xff09;和控制反转&#xff08…...

海宁网站设计公司/南京网站建设

补题链接&#xff1a; https://acm.hdu.edu.cn/showproblem.php?pid7059 考点: 线段树 解题思路可参考&#xff1a;https://www.cnblogs.com/lipoicyclic/p/15139116.html 但是与下面的代码不同的是: 下面代码考虑的是flag true 代表[l, r]区间内ai为0&#xff0c;即高位和…...

政府网站建设的基本原则/杭州seo搜索引擎优化公司

records简单了解&#xff1a;Records 是一个非常简单但功能强大的库&#xff0c;用于对大多数关系数据库进行原始SQL查询。只需编写SQL。没有钟声&#xff0c;没有哨声。使用可用的标准工具&#xff0c;这一常见任务可能会令人惊讶地困难。该库努力使此工作流程尽可能简单&…...

湖南常德米粉/优化搜索引擎

2019独角兽企业重金招聘Python工程师标准>>> 从高德地图下载开发者Demo&#xff0c;或者是百度地图Demo&#xff0c;会发现这些应用只能在ARM模拟器上运行&#xff0c;速度很慢&#xff0c;无法在x86模拟器上运行。对于开发人员来说&#xff0c;这是很苦恼的事情。小…...

网站做著作权/网时代教育培训机构怎么样

创建节点、关系 创建节点&#xff08;小明&#xff09;&#xff1a;create (n:people{name:’小明’,age:’18’,sex:’男’}) return n; 创建节点&#xff08;小红&#xff09;: create (n:people{name:’小红’,age:’18’,sex:’女’}) return n; 创建关系&#xff08;小明送…...

服装网站开发目的/洛阳seo网站

Lua 文件 I/O Lua I/O 库用于读取和处理文件。分为简单模式&#xff08;和C一样&#xff09;、完全模式。 简单模式&#xff08;simple model&#xff09;拥有一个当前输入文件和一个当前输出文件&#xff0c;并且提供针对这些文件相关的操作。完全模式&#xff08;complete mo…...

电子商务网站建设书籍/湖南中高风险地区

有关mysql索引的创建与管理。1&#xff0c;为出现在where子句的字段建一个索引。首先&#xff0c;创建如下表&#xff1a; 代码示例:CREATE TABLE mytable (id serial primary key,category_id int not null default 0,user_id int not null default 0,adddate int not null de…...