深度分析React源码中的合成事件

news2024/9/20 18:28:27

热身准备

明确几个概念

React@17.0.3版本中:

  • 所有事件都是委托在id = root的DOM元素中(网上很多说是在document中,17版本不是了);
  • 在应用中所有节点的事件监听其实都是在id = root的DOM元素中触发;
  • React自身实现了一套事件冒泡捕获机制;
  • React实现了合成事件SyntheticEvent
  • React17版本不再使用事件池了(网上很多说使用了对象池来管理合成事件对象的创建销毁,那是16版本及之前);
  • 事件一旦在id = root的DOM元素中委托,其实是一直在触发的,只是没有绑定对应的回调函数;

image.png 盗用一张官方图,按官方解释,之所以会将事件委托从document中移到id = root的DOM元素,是为了可以更加安全地进行新旧版本 React 树的嵌套

感兴趣的可以访问:React中文网站 。

事件系统角色划分

  • 事件注册:registerEvents
  • 事件监听:listenToAllSupportedEvents
  • 事件合成:SyntheticBaseEvent
  • 事件派发:dispatchEvent

事件注册

事件注册是自执行的,也就是React自身进行调用的:

// 注册React事件
registerSimpleEvents();  
registerEvents$2();
registerEvents$1();
registerEvents$3();
registerEvents();

React事件就是在组件中调用的onClick这种写法的事件。上面分为5个函数写,主要是区分不同的事件注册逻辑,但是最后都会添加到allNativeEventsSet数据结构中

registerSimpleEvents

这里会注册大部分事件,它们在React被定义为顶级事件。

它们分为三类:

  • 离散事件:discreteEvent,常见的如:click, keyup, change
  • 用户阻塞事件:userBlocking,常见的如:dragEnter, mouseMove, scroll
  • 连续事件:continuous,常见的如:error, progress, load,
    它们的优先级排序:

0:离散事件, 1:用户阻塞事件, 2:连续事件

它们会注册冒泡和捕获阶段两个事件。

registerEvents$2

注册类似onMouseEnteronMouseLeave单阶段事件,只注册冒泡阶段事件。

registerEvents$1

注册onChange相关事件,注册冒泡和捕获阶段两个事件。

registerEvents$3

注册onSelect相关事件,注册冒泡和捕获阶段两个事件。

registerEvents

注册onBeforeInputonCompositionUpdate等相关事件,注册冒泡和捕获阶段两个事件。

事件监听

在React源码系列之二:React的渲染机制曾提到过,React在开始渲染前,会为应用创建一个fiberRoot作为应用的根节点。在创建fiberRoot还会做一件事,就是

listenToAllSupportedEvents(rootContainerElement);

从字面就能理解这个函数是做事件监听的,其中rootContainerElement参数就是应用中的id = root的DOM元素。相关参考视频讲解:进入学习

该函数主要遍历上面事件注册添加到allNativeEvents的事件,按照一定规则,区分冒泡阶段,捕获阶段,区分有无副作用进行监听,监听的api还是addEventListener:

// 监听冒泡阶段事件
function addEventBubbleListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, false);
  return listener;
}
// 监听捕获阶段事件
function addEventCaptureListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, true);
  return listener;
}

代码中的target就是id = root的DOM元素。

注意,上面监听的listener是一个事件派发器,并不是真实的浏览器事件或你写的事件回调函数。 不要搞混淆了。

事件派发

上面提到,事件一旦在id = root的DOM元素中委托,其实是一直在触发的,只是没有绑定对应的回调函数。

意思是,当我们把鼠标移入我们的应用页面中时,这时就在派发事件了,因为页面的DOM元素是有监听mousemove之类的事件的。

那问题来了,React是如何得知我们给事件绑定了回调函数并触发对应的回调函数的?

带着这个问题我们来研究下事件派发

要讲事件派发,还得提下事件监听阶段监听的listener,它实际是下面这玩意:

function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
  var eventPriority = getEventPriorityForPluginSystem(domEventName);
  var listenerWrapper;

  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;

    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;

    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }

  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}

和事件注册一样,listener也分为dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三种。它们之间的主要区别是执行优先级,还有discreteEvent涉及到要清除之前的discreteEvent问题,所以做了区分。但是它们最后都会调用dispatchEvent

所以事件派发的角色应该是dispatchEvent

function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {

  var allowReplay = true;

  allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
  // 如果有离散事件正在执行,会排队,顺序执行
  if (allowReplay && hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName)) {
    domEventName, eventSystemFlags, targetContainer, nativeEvent);
    return;
  }
  // 尝试事件派发,如果成功,就不用执行下面的代码了
  var blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
  // 尝试事件派发成功
  if (blockedOn === null) {
    if (allowReplay) {
      // 清除连续事件队列
      clearIfContinuousEvent(domEventName, nativeEvent);
    }

    return;
  }

  if (allowReplay) {
    if (isReplayableDiscreteEvent(domEventName)) {

      queueDiscreteEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent);
      return;
    }

    if (queueIfContinuousEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent)) {
      return;
    } 

    clearIfContinuousEvent(domEventName, nativeEvent);
  } 

  dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}

介绍下dispatchEvent的几个参数:

  • domEventName: DOM事件名称,如:click,不是onClick
  • eventSystemFlags:事件系统标记;
  • targetContainerid=root的DOM元素;
  • nativeEvent:原生事件(来自addEventListener);

attemptToDispatchEvent中, 根据nativeEvent.target找到真正触发事件的DOM元素,并根据DOM元素找到对应的fiber节点,判断fiber节点的类型以及是否已渲染来决定是否要派发事件。

在一系列判断通过后,就开始真正的事件处理了:

function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  // 获取触发事件的DOM元素
  var nativeEventTarget = getEventTarget(nativeEvent);
  // 初始化事件派发队列
  var dispatchQueue = [];
  // 合成事件
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // 按事件派发队列执行事件派发
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents$5中会进行事件合成,放在下面单独讲。

processDispatchQueue会根据事件阶段(冒泡或捕获)来决定是正序还是倒序遍历合成事件中的listeners

接下来就比较简单了。 遍历listeners执行上面的listener

合成事件

在合成事件中,会根据domEventName来决定使用哪种类型的合成事件。

click为例,当我们点击页面的某个元素时,React会根据原生事件nativeEvent找到触发事件的DOM元素和对应的fiber节点。并以该节点为孩子节点往上查找,找到包括该节点及以上所有的click回调函数创建dispatchListener,并添加到listeners数组中。

// dispatchListener
{
    instance: instance,  // 事件所在的fiber节点
    listener: listener,  // 事件回调函数
    currentTarget: currentTarget  // 事件对应的DOM元素
  }

当向上查找完成后,会基于click类型的合成事件类创建事件

// 创建合成事件实例
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
// 事件派发队列添加事件
dispatchQueue.push({
  event: _event,   // 合成事件实例
  listeners: _listeners  // 同类型事件的集合数组
});

看下SyntheticEventCtor

// Interface根据事件类型有所不同
function createSyntheticEvent(Interface) {
  // 合成事件构造函数
  function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;
    // React根据不同事件类型写了对应的属性接口,这里基于接口将原生事件上的属性clone到构造函数中
    for (var _propName in Interface) {... }

    var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;

    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }

    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  _assign(SyntheticBaseEvent.prototype, {
    // 阻止默认事件
    preventDefault: function () {...},
    // 阻止捕获和冒泡阶段中当前事件的进一步传播
    stopPropagation: function () {...},
    // 合成事件不使用对象池了,这个事件是空的,没有意义,保存是为了向下兼容不报错。
    persist: function () {},

    isPersistent: functionThatReturnsTrue
  });

  return SyntheticBaseEvent;
}

看到这里,我们基本能弄明白合成事件是个什么东西了。

React合成事件是将同类型的事件找出来,基于这个类型的事件,React通过代码定义好的类型事件的接口和原生事件创建相应的合成事件实例,并重写了preventDefaultstopPropagation方法。

这样,同类型的事件会复用同一个合成事件实例对象,节省了单独为每一个事件创建事件实例对象的开销,这就是事件的合成。

捕获和冒泡

事件派发分为两个阶段执行, 捕获阶段和冒泡阶段。

在上面事件合成中讲过,React会根据事件触发的fiber节点向上查找,将上面的同类型事件添加到队列中,这样天然就有了一个冒泡的顺序,从最底层向上冒泡。如果倒序过来遍历就是捕获的顺序。

所以,React实现冒泡和捕获就很简单了,只需要根据事件派发的阶段,判断是冒泡阶段还是捕获阶段来决定是正序遍历listeners还是倒序遍历就行了。

总结

说是讲React的合成事件,实际上讲了React的事件系统。做下总结:

React的事件系统分为几个部分:

  • 事件注册;

  • 事件监听;

  • 事件合成;

  • 事件派发;
    事件系统流程:

  1. React代码执行时,内部会自动执行事件的注册;

  2. 第一次渲染,创建fiberRoot时,会进行事件的监听,所有的事件通过addEventListener委托在id=root的DOM元素上进行监听;

  3. 在我们触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象;

  4. 最后进行事件的派发,执行我们代码中的事件回调函数;

当然,由于篇幅问题,这里也是对React事件系统的一个精简剖析,可能忽略了一些地方,欢迎指正。

看完这篇文章, 我们可以弄明白下面这几个问题:

  1. React事件委托在哪?
  2. React合成事件是什么?
  3. React合成事件是怎么实现的?
  4. React是怎么实现冒泡和捕获的?
  5. React合成事件是使用的原生事件吗?
  6. React事件系统分为哪几个部分?

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

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

相关文章

【MySQL 第十一天 创建和存储|复合结构的存储|存储过程和函数的区别】

【MySQL 第十一天 创建和存储|复合结构的存储|存储过程和函数的区别】【1】mysql储存过程及语法结构【1.1】mysql过程体【2】mysql创建和使用存储过程【2.1】mysql创建无参的存储过程【2.2】mysql创建有参的输入输出存储过程【3】mysql删除存储过程【4】mysql创建复合结构的存储…

专精特新小巨人的认定条件

奖励:对新认定的专精特新“小巨人”企业,聊城市财政最高一次性奖励50万元。其他地区各有不同。 认定条件 专精特新“小巨人”企业认定需同时满足专、精、特、新、链、品六个方面指标。 (一)专业化指标:坚持专业化发展道路,长期…

大学生体育运动网页设计模板代码 DIV布局校园运动网页作业成品 HTML学校网页制作模板 学生简单体育运动网站设计成品

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

C++知识精讲14 | 算法篇之二分查找算法

博主主页:Cool Kid~Yu仙笙_C领域博主🦄 目录 二分查找定义 二分查找效率 二分查找与遍历的对比 二分查找的限制性 二分查找的限制性(总结) 二分查找搭建 循环实现二分查找 循环二分查找基本框架: 循环二分查找源码&am…

【苹果家庭iMessage推送】Aupperpushslcertificate或ProductPushsCertificate证书不可以过期

推荐内容IMESSGAE相关 作者推荐内容iMessage苹果推软件 *** 点击即可查看作者要求内容信息作者推荐内容1.家庭推内容 *** 点击即可查看作者要求内容信息作者推荐内容2.相册推 *** 点击即可查看作者要求内容信息作者推荐内容3.日历推 *** 点击即可查看作者要求内容信息作者推荐…

前端实现给文字添加动态背景

📋 个人简介 💖 作者简介:大家好,我是阿牛,全栈领域优质创作者。😜📝 个人主页:馆主阿牛🔥🎉 支持我:点赞👍收藏⭐️留言&#x1f4d…

【JAVA开发】提高开发效率的工具分享

代码管理工具 仓库: GitHub or GitLab or 本地仓库 版本控制: git or svn 推荐gitLabgit 多分支敏捷开发 开发工具 IDEA 最方便开发工具了 当然如何你是全栈也可以考虑使用VS(visual studio)、HBuider、AS(android studio) 文本工具 Sublime text …

Redis数据结构之整数集合

目录 基本数据结构 例子 升级 升级之后新元素的摆放位置 好处 降级 整数集合可以理解为一个有序(升序)的不允许元素重复的数组。 基本数据结构 intset会根据 编码格式分配空间。 例子 升级 当新添加的元素超过了当前编码格式所能 表示的范围&…

Linux常用命令工具

1、查找特定文本中的特定字符 cat filename | grep myStr eg: cat .config | grep KCOV 2、查找特定文本中的特定字符并打印具体行数 cat filename | grep -n myStr eg:: cat .config | grep -n KCOV 3、查找一个文件夹中的特定字符 grep -r myStr filedir eg: grep -r __NR_…

Mysql注入

💪💪mysql注入前言1.mysql之union注入1.1.判断是否有注入:1.2.信息收集1.3.进行注入1.4.完整注入实列2. mysql 跨库注入:2.1第一步就是找到数据库2.2第二步就是注入3.自己写union注入4.其他注入操作前言 💎&#x1f48…

JVM笔记:垃圾回收及垃圾回收算法

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢…

【Vue项目回顾】网络模块的封装

选择什么网络模块 选择一: 传统的Ajax是基于XMLHttpRequest(XHR) 为什么不用它呢? 非常好解释, 配置和调用方式等非常混乱.编码起来看起来就非常蛋疼.所以真实开发中很少直接使用, 而是使用jQuery-Ajax 选择二: 在前面的学习中, 我们经常会使用jQuery-Ajax 相对于传统的A…

【PyTorch深度学习项目实战100例】—— 基于Pytorch的语音情感识别系统 | 第71例

前言 大家好,我是阿光。 本专栏整理了《PyTorch深度学习项目实战100例》,内包含了各种不同的深度学习项目,包含项目原理以及源码,每一个项目实例都附带有完整的代码+数据集。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmPy…

C#和Python使用C++编译的dll

C代码如下: extern "C" __declspec(dllexport) int __stdcall add(int a, int b) {return a b; } 因为是显示链接,所以只需要获得生成的dll即可 因为C#无法直接调用C的dll,所以我们使用了extern"C" 使用vs自带的工具x86_x64 Cross…

Vue中KeepAlive 原理与源码分析

概念 keep-alive是Vue的一个内置实例,用于缓存组件。当keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是摧毁它们。keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链之中。如需要…

网课查题API接口-搜题api

网课查题API接口-搜题api 本平台优点: 多题库查题、独立后台、响应速度快、全网平台可查、功能最全! 1.想要给自己的公众号获得查题接口,只需要两步! 2.题库: 题库:题库后台(点击跳转&#x…

小学生python游戏开发pygame--设置内容整理

游戏开发,相关设置内容单独放在一个文件中 如长宽,大小,颜色等起名shezhi.py,如下: # _*_ coding: UTF-8 _*_ # 开发团队: 信息化未来 # 开发人员: Administrator # 开发时间:2022…

微信小程序中如何实现省市区街道四级地址级联选择

大家好,我是雄雄。 前言 微信小程序中支持省市区地址级联吗?微信小程序中的地址级联最多支持到几级? 今天,我们就来看看,微信小程序中的地址级联的使用,以及一些坑…希望大家看完之后能避免踩坑啊。 省市…

JS(受人以鱼 不如受人以渔)第十七课

比你优秀的人都比你努力,你有什么理由不去努力。基础来自己的累秒累天累月的积累 没有一个人是从天而降的天才,也没有哪个人想做一个一生贫庸的人。今天我想说 受人以鱼 不如受人以渔 推荐几个官网可以自己主动去学习 JavaScript中文网-JavaScript教程…

虚拟物品(游戏)交易平台的设计与实现(Java+SSM+MySQL)

目 录 摘 要 I Abstract II 第1章 绪论 1 1.1选题背景及意义 1 1.2研究现状 1 1.3研究主要内容 2 第2章 相关技术介绍 4 1.1 SSM的技术原理 4 1.1.1 SSM语言及其特点 4 1.1.2 Java及Java Servlets概述 6 1.1.3 JavaBean简介 6 1.2 服务器配置 7 1.2.1 Tomcat安装及配置 7 1.2.2…