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

 更新时间:2022年11月10日 14:59:33   作者:goClient1992  
合成事件不是浏览器本身触发的事件,自己创建和触发的事件。本文将从源码角度带大家一起深入了解下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合成事件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • React新扩展函数setState与lazyLoad及hook介绍

    React新扩展函数setState与lazyLoad及hook介绍

    这篇文章主要介绍了React新扩展函数setState与lazyLoad及hook,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • 通过示例讲解Remix 设计哲学理念

    通过示例讲解Remix 设计哲学理念

    这篇文章主要为大家通过示例讲解了Remix 设计哲学理念,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • React根据当前页面路由进行自动高亮示例代码

    React根据当前页面路由进行自动高亮示例代码

    要根据当前页面路由自动高亮顶部菜单项,可以使用 React Router 的 useLocation 钩子来获取当前路径,并根据路径动态设置菜单项的高亮效果,本文给大家介绍了一个完整的示例,展示如何根据当前页面路由自动高亮顶部菜单项,需要的朋友可以参考下
    2024-07-07
  • 模块化react-router配置方法详解

    模块化react-router配置方法详解

    这篇文章主要介绍了模块化react-router配置方法详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-06-06
  • React渲染的优化方案

    React渲染的优化方案

    react的渲染机制是非常独特的,有别于 Vue 框架的渲染次数的优化计算,React 很久以来就有PureComponent、shouldUpdate,本文小编给大家介绍了React渲染的优化方案,需要的朋友可以参考下
    2024-08-08
  • react中用less的问题

    react中用less的问题

    本文主要介绍了react中用less的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-04-04
  • Objects are not valid as a React child报错解决

    Objects are not valid as a Rea

    这篇文章主要为大家介绍了Objects are not valid as a React child报错解决方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • ReactDOM 隐藏特性详解

    ReactDOM 隐藏特性详解

    这篇文章主要为大家介绍了ReactDOM 隐藏特性详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • React函数组件useContext useReducer自定义hooks

    React函数组件useContext useReducer自定义hooks

    这篇文章主要为大家介绍了React函数组件useContext useReducer自定义hooks示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • webpack+react+antd脚手架优化的方法

    webpack+react+antd脚手架优化的方法

    本篇文章主要介绍了webpack+react+antd脚手架优化的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-04-04

最新评论