我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

热身准备

明确几个概念

React@17.0.3版本中:

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

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

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

事件系统角色划分

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

事件注册

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

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

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

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.事件注册;

2.事件监听;

3.事件合成;

4.事件派发; 事件系统流程:

  • React代码执行时,内部会自动执行事件的注册;
  • 第一次渲染,创建fiberRoot时,会进行事件的监听,所有的事件通过addEventListener委托在id=root的DOM元素上进行监听;
  • 在我们触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象;
  • 最后进行事件的派发,执行我们代码中的事件回调函数;

到此这篇关于深入分析React源码中的合成事件的文章就介绍到这了,更多相关React合成事件内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

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

合成事件不是浏览器本身触发的事件,自己创建和触发的事件。本文将从源码角度带大家一起深入了解下React中的合成事件,需要的可以参考一下
2022-11-13

react源码合成事件深入解析

这篇文章主要为大家介绍了react源码合成事件深入解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-06

React实现合成事件的源码分析

React 中的事件,是对原生事件的封装,叫做合成事件。抽象出一层合成事件,是为了做兼容,抹平不同浏览器之间的差异。本文将从事件绑定和事件触发角度,带大家解读下源码,感兴趣的可以了解一下
2022-12-08

React深入分析useEffect源码

useEffect是react v16.8新引入的特性。我们可以把useEffect hook看作是componentDidMount、componentDidUpdate、componentWillUnmounrt三个函数的组合
2022-11-13

React深入浅出分析Hooks源码

在react类组件(class)写法中,有setState和生命周期对状态进行管理,但是在函数组件中不存在这些,故引入hooks(版本:>=16.8),使开发者在非class的情况下使用更多react特性
2022-11-13

React深入分析更新的创建源码

React组件分为函数组件与class组件;函数组件是无状态组件,class称为类组件;函数组件只有props,没有自己的私有数据和生命周期函数;class组件有自己私有数据(this.state)和生命周期函数
2023-01-14

react中合成事件与原生事件的示例分析

小编给大家分享一下react中合成事件与原生事件的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1. 原生事件原生事件就是js的原生事件,如通过docum
2023-06-15

Android事件分发机制深入刨析原理及源码

Android 的事件分发机制大体可以分为三部分:事件生产、事件分发 、事件消费。事件的生产是由用户点击屏幕产生,我们这次着重分析事件的分发和消费,因为事件分发和处理联系的过于紧密,这篇文章将把事件的分发和消费放在一起分析
2023-05-16

深入React 18源码useMemo useCallback memo用法及区别分析

这篇文章主要为大家介绍了React 18源码深入分析useMemo useCallback memo用法及区别,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-05-17

React中的合成事件怎么实现

这篇文章主要介绍了React中的合成事件怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇React中的合成事件怎么实现文章都会有所收获,下面我们一起来看看吧。1 事件三个阶段 捕获、目标、处理 (具体百度
2023-07-05

Vue组件中的自定义事件源码分析

这篇文章主要介绍“Vue组件中的自定义事件源码分析”,在日常操作中,相信很多人在Vue组件中的自定义事件源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Vue组件中的自定义事件源码分析”的疑惑有所帮助!
2023-06-29

React中合成事件的原理是什么

本篇内容介绍了“React中合成事件的原理是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!事件介绍什么是事件事件是在编程时系统内发生的动
2023-07-05

React中的合成事件是什么原理

React中的事件,是对原生事件的封装,叫做合成事件。这篇文章主要通过几个简单的示例为大家详细介绍一下React中的合成事件,感兴趣的可以了解一下
2023-02-23

Laravel中的事件溯源实例代码分析

这篇文章主要介绍了Laravel中的事件溯源实例代码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Laravel中的事件溯源实例代码分析文章都会有所收获,下面我们一起来看看吧。我们将新建一个 Laravel
2023-07-04

深入理解 Android事件分发机制源码(基于9.0)

touch事件最开始从Activity 的 dispatchTouchEvent() 方法开始的/frameworks/base/core/java/android/app/Activity.java /*** Called to proc
2022-06-06

react源码中的生命周期和事件系统实例解析

这篇文章主要为大家介绍了react源码中的生命周期和事件系统实例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-03

编程热搜

目录