React事件和原生事件的执行顺序

news2025/1/18 8:44:20

React在内部对事件做了统一的处理,合成事件是一个比较大的概念

为什么要有合成事件

  1. 在传统的事件里,不同的浏览器需要兼容不同的写法,在合成事件中React提供统一的事件对象,抹平了浏览器的兼容性差异
  2. React通过顶层监听的形式,通过事件委托的方式来统一管理所有的事件,可以在事件上区分事件优先级,优化用户体验

React在合成事件上对于16版本和17版本的合成事件有很大不同,也会简单聊聊区别。

概念

事件委托

事件委托的意思就是可以通过给父元素绑定事件委托,通过事件对象的target属性可以获取到当前触发目标阶段的dom元素,来进行统一管理

比如写原生dom循环渲染的时候,要给每一个子元素都添加dom事件,这种情况最简单的方式就是通过事件委托在父元素做一次委托,通过target属性判断区分做不同的操作

事件监听

事件监听主要用到了addEventListener这个函数,具体怎么用可以点击进行查看 事件监听和事件绑定的最大区别就是事件监听可以给一个事件监听多个函数操作,而事件绑定只有一次

// 可以监听多个,不会被覆盖
eventTarget.addEventListener('click', () => {});
eventTarget.addEventListener('click', () => {});

eventTarget.onclick = function () {};
eventTarget.onclick = function () {}; // 第二个会把第一个覆盖

事件执行顺序

<div>
  <span>点我</span>
</div>

当点击span标签的时候会经过这么三个过程,在路径内的元素绑定的事件都会进行触发

捕获阶段 => 目标阶段 => 冒泡阶段

 

合成事件

在看之前先看一下这几个问题

  • 原生事件和合成事件的执行顺序是什么?
  • 合成事件在什么阶段下会被执行?
  • 阻止原生事件的冒泡,会影响到合成事件的执行吗?
  • 阻止合成事件的冒泡,会影响到原生事件的执行吗?
import React, { useRef, useEffect } from "react";
import "./styles.css";

const logFunc = (target, isSynthesizer, isCapture = false) => {
    const info = `${isSynthesizer ? "合成" : "原生"}事件,${
        isCapture ? "捕获" : "冒泡"}阶段,${target}元素执行了`;
    
    console.log(info);
};

const batchManageEvent = (targets, funcs, isRemove = false) => {
    targets.forEach((target, targetIndex) => {
        funcs[targetIndex].forEach((func, funcIndex) => {
            target[isRemove ? "removeEventListener" : "addEventListener"](
                "click",
                func,
                !funcIndex
            );
        });
    });
};

export default function App() {
    const divDom = useRef();
    const h1Dom = useRef();
    useEffect(() => {
    
        const docClickCapFunc = () => logFunc("document", false, true);
        const divClickCapFunc = () => logFunc("div", false, true);
        const h1ClickCapFunc = () => logFunc("h1", false, true);
        const docClickFunc = () => logFunc("document", false);
        const divClickFunc = () => logFunc("div", false);
        const h1ClickFunc = () => logFunc("h1", false);

        batchManageEvent(
            [document, divDom.current, h1Dom.current],
            [
                [docClickCapFunc, docClickFunc],
                [divClickCapFunc, divClickFunc],
                [h1ClickCapFunc, h1ClickFunc]
            ]
        );

        return () => {
            batchManageEvent(
                   [document, divDom.current, h1Dom.current],
                [
                    [docClickCapFunc, docClickFunc],
                    [divClickCapFunc, divClickFunc],
                    [h1ClickCapFunc, h1ClickFunc]
                ],
                true
            );
        };
    }, []);

    return (
        <div
          ref={divDom}
          className="App1"
          onClickCapture={() => logFunc("div", true, true)}
          onClick={() => logFunc("div", true)}
        >
          <h1
            ref={h1Dom}
            onClickCapture={() => logFunc("h1", true, true)}
            onClick={() => logFunc("h1", true)}
          >
            Hello CodeSandbox
          </h1>
        </div>
    );
}

看这个例子,当点击h1的时候

会先执行原生事件事件流,当执行到document的冒泡阶段的时候做了个拦截,在这个阶段开始执行合成事件

用一个图简单描述一下

 

 

知道上面的概念,那回答开始阶段的后面两个问题

当把上面的demo的原生divstopPropagation()  方法调用阻止捕获和冒泡阶段中当前事件的进一步传播,会阻止后续的所有事件执行

可以看到,当阻止之后,点击h1,事件流运行到div的捕获阶段就不触发了,后续的所有的包括合成事件也都不会触发

那当给合成事件的事件流中断了会发生什么呢?

可以看到运行到捕获阶段的div之后被阻止传播了,后续的所有合成事件都不会执行了,但是原生的document冒泡还是会执行完。

模拟阶段

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, maximum-scale=1, user-scalable=no" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <link href="favicon.ico" type="image/x-icon" rel="icon" />
    <title>浅谈React合成事件</title>
  </head>
  <body>
    <div id="wrapper">
      <h1 id="content">hello</h1>
    </div>
  </body>
  <script>
    const logFunc = (target, isSynthesizer, isCapture = false) => {
      const info = `${isSynthesizer ? '合成' : '原生'}事件,${isCapture ? '捕获' : '冒泡'}阶段,${target}元素执行了`;
      console.log(info);
    };
    // document的派发事件函数
    const dispatchEvent = currentDom => {
      let current = currentDom;
      let eventCallbacks = []; // 存储冒泡事件回调函数
      let eventCaptureCallbacks = []; // 存储冒泡事件回调函数
      // 收集事件流一路上的所有回调函数
      while (current) {
        if (current.onClick) {
          eventCallbacks.push(current.onClick);
        }
        if (current.onClickCapture) {
          // 捕获阶段由外到内,所以需要把回调函数放到数组的最前面
          eventCaptureCallbacks.unshift(current.onClickCapture);
        }
        current = current.parentNode;
      }
      // 执行调用
      eventCaptureCallbacks.forEach(callback => callback());
      eventCallbacks.forEach(callback => callback());
    };
    const wrapperDom = document.getElementById('wrapper');
    const contentDom = document.getElementById('content');

    // 一路上注册原生事件
    document.addEventListener('click', () => logFunc('document', false, true), true);
    wrapperDom.addEventListener('click', () => logFunc('div', false, true), true);
    contentDom.addEventListener('click', () => logFunc('h1', false, true), true);
    contentDom.addEventListener('click', () => logFunc('h1', false));
    wrapperDom.addEventListener('click', () => logFunc('div', false));
    document.addEventListener('click', e => {
      dispatchEvent(e.target); // 这里收集一路上的事件进行派发
      logFunc('document', false);
    });

    // 模拟合成事件
    wrapperDom.onClick = () => logFunc('div', true);
    wrapperDom.onClickCapture = () => logFunc('div', true, true);
    contentDom.onClick = () => logFunc('h1', true);
    contentDom.onClickCapture = () => logFunc('h1', true, true);
  </script>
</html>

点击h1可以看到一路上的注册的所有事件已经执行了

React16document上加的统一的拦截判发事件会在一定情况下出问题,下面举个例子简单说明一下

16案例 

import React, { useEffect, useState } from 'react';
import './styles.css';

const Modal = ({ onClose }) => {
  useEffect(() => {
    document.addEventListener('click', onClose);
    return () => {
      document.removeEventListener('click', onClose);
    };
  }, [onClose]);
  return (
    <div
      style={{ width: 300, height: 300, backgroundColor: 'red' }}
      onClick={e => {
        e.stopPropagation();
        // e.nativeEvent.stopImmediatePropagation();
      }}
    >
      Modal
    </div>
  );
};

function App() {
  const [visible, setVisible] = useState(false);
  return (
    <div className="App">
      <button
        onClick={() => {
          setVisible(true);
        }}
      >
        点我弹出modal
      </button>
      {visible && <Modal onClose={() => setVisible(false)} />}
    </div>
  );
}
export default App;

写完之后点击按钮Modal被弹出来, 但是点击modal里面的内容modal就隐藏了,添加阻止事件流函数还是不行

原因就是点击之后,事件冒泡到document上,同时也执行了他身上挂载的方法,解决办法就是给点击事件添加 e.nativeEvent.stopImmediatePropagation();

stopImmediatePropagationstopPropagation的区别就是,前者会阻止当前节点下所有的事件监听的函数,后者不会

react17及之后做了什么改变呢

16和17的区别

17版本中,React把事件节点绑定函数绑定在了render的根节点上,避免了上述的问题,

用上面的demo的在线案例把版本改成17之后,可以发现事件的执行顺序变了

模拟17版本

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, maximum-scale=1, user-scalable=no" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <link href="favicon.ico" type="image/x-icon" rel="icon" />
    <title>浅谈React合成事件</title>
  </head>
  <body>
    <div id="root">
      <div id="wrapper">
        <h1 id="content">hello</h1>
      </div>
    </div>
  </body>
  <script>
    const logFunc = (target, isSynthesizer, isCapture = false) => {
      const info = `${isSynthesizer ? '合成' : '原生'}事件,${isCapture ? '捕获' : '冒泡'}阶段,${target}元素执行了`;
      console.log(info);
    };
    // document的派发事件函数
    const dispatchEvent = (currentDom, useCapture = false) => {
      let current = currentDom;
      let eventCallbacks = []; // 存储冒泡事件回调函数
      const eventTypeName = useCapture ? 'onClickCapture' : 'onClick'; // 冒泡事件或者捕获事件的名称
      const actionName = useCapture ? 'unshift' : 'push';
      while (current) {
        if (current[eventTypeName]) {
          eventCallbacks[actionName](current[eventTypeName]);
        }
        current = current.parentNode;
      }
      eventCallbacks.forEach(callback => callback());
    };
    const wrapperDom = document.getElementById('wrapper');
    const contentDom = document.getElementById('content');
    const root = document.getElementById('root');

    // 一路上注册原生事件
    document.addEventListener('click', () => logFunc('document', false, true), true);
    root.addEventListener(
      'click',
      e => {
        dispatchEvent(e.target, true);
        logFunc('root', false, true);
      },
      true
    );
    wrapperDom.addEventListener('click', () => logFunc('div', false, true), true);
    contentDom.addEventListener('click', () => logFunc('h1', false, true), true);
    contentDom.addEventListener('click', () => logFunc('h1', false));
    wrapperDom.addEventListener('click', () => logFunc('div', false));
    root.addEventListener('click', e => {
      dispatchEvent(e.target); // 这里收集一路上的事件进行派发
      logFunc('root', false);
    });
    document.addEventListener('click', () => logFunc('document', false));
    // 模拟合成事件
    wrapperDom.onClick = () => logFunc('div', true);
    wrapperDom.onClickCapture = () => logFunc('div', true, true);
    contentDom.onClick = () => logFunc('h1', true);
    contentDom.onClickCapture = () => logFunc('h1', true, true);
  </script>
</html>

区别就是在外层增加了一个root模拟根节点,修改了dispatchEvent的逻辑

可以看到,效果已经和17版本的一样了

回看16demo,切换版本到17,当切换到17的时候,用stopPropagation就可以解决问题了, 原因就是他在root节点上绑定的事件冒泡函数,stopPropagation切断了事件流,不会流向到document身上了

总结

  • 16版本先执行原生事件,当冒泡到document时,统一执行合成事件,
  • 17版本在原生事件执行前先执行合成事件捕获阶段,原生事件执行完毕执行冒泡阶段的合成事件,通过根节点来管理所有的事件

原生的阻止事件流会阻断合成事件的执行,合成事件阻止后也会影响到后续的原生执行

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

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

相关文章

ROS学习寄录之环境搭建

1 创建ROS工作空间 1.1 创建工作空间 &#xff08;1&#xff09;创建工作空间 mkdir catkin_ws &#xff08;2&#xff09;进入catkin_ws文件夹&#xff0c;然后创建一个src文件夹 cd catkin_ws mkdir src &#xff08;3&#xff09;进入src文件夹&#xff0c;生成CMakeL…

JavaScript typeof

文章目录JavaScript typeof, null, 和 undefinedtypeof 操作符NullUndefinedUndefined 和 Null 的区别JavaScript typeof, null, 和 undefined JavaScript typeof, null, undefined, valueOf()。 typeof 操作符 你可以使用 typeof 操作符来检测变量的数据类型。 实例 typeof …

2.2 标识符与关键字

文章目录1 标识符2 关键字1 标识符 标识符可以简单的理解成一个名字。 在Java中&#xff0c;我们需要给代码中的很多元素起名&#xff0c;包括类名、方法名、字段名、变量名等等。我们给对应元素起的名称就被称为标识符&#xff0c;一个正确的标识符需要遵循以下规则&#xff…

【蓝桥杯】简单数论4——丢番图方程

1、二元线性丢番图方程 方程ax by c被称为二元线性丢番图方程&#xff0c;其中a、b、c是已知整数&#xff0c;x、y是变量,问是否有整数解。 ax by c实际上是二维x-y平面上的一条直线&#xff0c;这条直线上如果有整数坐标点&#xff0c;方程就有解&#xff0c;如果没有整数坐…

【算法竞赛 5】动态规划 ——— 闫氏DP分析法(从集合角度来分析DP问题——01背包)

目录 Description 输入格式 输出格式 数据范围 输入样例 输出样例&#xff1a; 题解 状态表示 状态计算 AC_Code 优化后代码 Description 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品…

2.4.1 整数型

文章目录1.整型基本数据类型介绍2.byte 数据类型3.short 数据类型4.int 数据类型5.long 数据类型1.整型基本数据类型介绍 整型用于表示没有小数部分的数字&#xff0c;比如1&#xff0c;2&#xff0c;3等&#xff0c;其允许是负数&#xff0c;JAVA共提供了4种整型数据类型&…

电阻抗成像OpenEIT 番外篇 简单算法

算法意义 C→Y→V→eC\rightarrow Y \rightarrow V \rightarrow eC→Y→V→e 符号 符号含义–Ω\OmegaΩ研究图像区域∂Ω\partial\Omega∂Ω研究图像区域的边界ϕ(x,y)\phi(x,y)ϕ(x,y)代求电导率σ(x,y)\sigma(x,y)σ(x,y)节点电压 e 电导率jn(x,y)j_n(x,y)jn​(x,y)注入表…

C++ 多线程12:内存模型(stdmemory_order)

cpp 多线程&#xff1a;内存模型(std::memory_order) 文章目录cpp 多线程&#xff1a;内存模型(std::memory_order)概念内存模型基础原子操作间的关系Synchronized-withHappens-beforestd::memory_orderRelaxed orderingRelease-Consume orderingRelease-Acquire orderingSeque…

UnityEditor编辑器扩展开发-自定义Shader入门

估计需要自定义Shader 的人不多下面内容就看看作为小白的我们&#xff0c;无从入手&#xff0c;当然首先看看 Amplify Shader Editor(ASE&#xff09;是如何实现Shader定义&#xff0c;从(ASE)的Shader代码&#xff0c;得知自定义原理&#xff08;代码&#xff09;//CustomEdit…

前端艺术之毛玻璃-倾斜-日历

前端艺术之毛玻璃-倾斜-日历描述项目效果index.htmlindex.css描述 项目描述开发语言HTML、JavaScript、CSS库dyCalendarJS、vanilla-tiltEdge108.0.1462.54 (正式版本) (64 位) 该项目中需要使用到的库有&#xff1a; dyCalendarJS vanilla-tilt.js 是 JavaScript 中的一个平…

C++ | 哈希 | 基于开散列结构的unordered系列容器模拟实现

文章目录unordered_map的封装所有接口的声明与实现operator[]重载unordered_set的封装上篇博客模拟实现了哈希的开散列结构&#xff0c;并且将迭代器与泛型进行了封装&#xff0c;至此我们可以将开散列作为底层结构对STL标准容器——unordered_map和unordered_set进行封装。但是…

使用Vue 简化 用户查询/添加功能

使用Vue简化 用户查询/添加功能1. 查询功能1.1 Vue核心对象&#xff1a;1.2 brand.html&#xff1a;1.3 selectAllServlet&#xff08;无变化&#xff09;&#xff1a;2. 添加功能2.1 addBrandhtml&#xff1a;2.2 Vue核心对象&#xff1a;2.3 addServlet&#xff08;无变化&am…

网关zuul源码解析==ZuulServlet

用法&#xff1a; 使用zuul网关&#xff0c;需要引入starter为 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> 同时在springboot启动类上加注解…

7、代码模板的使用

文章目录7、代码模板的使用7.1 查看Postfix Completion模板&#xff08;后缀补全&#xff09;7.2 查看Live Templates模板&#xff08;实时模板&#xff09;7.3 常用代码模板1 非空判断2 遍历数组和集合3 输出语句4 对象操作5 静态常量声明7.4 自定义代码模板1 自定义Postfix C…

Android入门第60天-MVVM中的Databinding与ListView结合使用

开篇 还记得我们进入Listview、GridView都是以一个layoutadapter组合在一起来实现的是吧&#xff1f;那么还记得我们的Adapter的写法么&#xff1f; 在我们的Adapter里提供了一个bindView方法 。 在调用时我们需要在Activity里把layout里的控件元素一个个传给这个Adapter。 在我…

【Linux】文件权限

本期我们来谈谈Linux上的权限&#xff1a;一、权限的概念在生活中我们处处都会遇到权限。权限是什么呢&#xff1f;下面是对于权限的定义&#xff1a;&#x1f4cc;权限&#xff1a;一件事是否允许被“谁”做&#x1f4cc;简化一下就是&#xff1a;权限人事物属性&#x1f4cb;…

【ROS-Navigation】—— Astar路径规划算法解析

文章目录前言1. 导航的相关启动和配置文件1.1 demo01_gazebo.launch1.2 nav06_path.launch1.3 nav04_amcl.launch1.4 nav05_path.launch1.5 move_base_params.yaml1.6 global_planner_params.yaml2. Astar路径规划算法解析2.1 astar.h2.2 astar.cpp参考文献前言 最近在学习ROS的…

《职场求生攻略》学习笔记 Day8

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…

JavaEE-多线程进阶

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录常见的锁策略乐观锁 vs 悲观锁轻量级锁 vs 重量级锁自旋锁 vs 挂起等待锁互斥锁 vs 读写锁公平锁 vs 非公平锁可重入锁 vs 不可…

恶意代码分析实战 8 恶意代码行为

8.1 Lab 11-01 代码分析 首先使用strings进行分析。 Gina是在 msgina.dll中的。 很多有关资源的函数。 关于注册表的函数。 使用ResourceHacker查看。 发现是一个PE文件。 保存为dll文件。 动态分析 启动Promon。 进入注册表查看。 这个恶意代码向磁盘释放了什么&…