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

目录
  • 事件绑定
  • 事件触发
  • 结尾

今天尝试学习 React 事件的源码实现。

React 版本为 18.2.0

React 中的事件,是对原生事件的封装,叫做合成事件。抽象出一层合成事件,是为了做兼容,抹平不同浏览器之间的差异。

下面会从两个方面进行源码的解读:

  • 事件绑定
  • 事件触发

事件绑定

首先是 React 项目过程启动时,调用 listenToAllSupportedEvents 方法,做合成事件的绑定。

// 对应方法 `ReactDOM.createRoot()
function createRoot(container, CreateRootOptions) {
  const rootContainerElement =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;

  // 绑定支持的事件
  listenToAllSupportedEvents(rootContainerElement);
}

listenToAllSupportedEvents 方法核心实现为:

function listenToAllSupportedEvents(rootContainerElement) {
  allNativeEvents.forEach(domEventName => {
    // 是否为会冒泡的事件
    if (!nonDelegatedEvents.has(domEventName)) {
      // 第二个参数是 isCapturePhaseListener
      // false 表示绑定在 “冒泡阶段”
      listenToNativeEvent(domEventName, false, rootContainerElement);
    }
    listenToNativeEvent(domEventName, true, rootContainerElement);
  }
}

allNativeEvents 是一个集合(Set),保存所有的原生 DOM 事件名。会 调用不同事件插件的初始化。

事件插件有:

  • SimpleEventPlugin;
  • EnterLeaveEventPlugin;
  • ChangeEventPlugin;
  • SelectEventPlugin;
  • BeforeInputEventPlugin

这些事件插件对象暴露有 registerEvents 和 extractEvents 方法。registerEvents 用于初始化原生事件和对应 React 事件,其中一个操作就是往 allNativeEvents 加原生事件名。

nonDelegatedEvents 也是一个集合(Set),保存不支持冒泡的事件,即不能进行事件委托,比如 cancel、scroll、媒体事件等。

对于支持冒泡的事件,捕获阶段和绑定阶段都绑定;对于不支持冒泡的事件,只绑定捕获阶段,且会 在目标元素上绑定事件。

listenToNativeEvent 里面调用了一层又一层的函数,人已经麻了。为了方便理解,这里将这些嵌套的函数拍平,丢掉一些次要的分支,得到下面核心代码:

function listenToNativeEvent() {
  const listener = dispatchEvent.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );

  if (isCapturePhaseListener) {
    // 捕获阶段的事件委托
    targetContainer.addEventListener(domEventName, listener, true);
  } else {
    // 冒泡阶段的事件委托
    targetContainer.addEventListener(domEventName, listener, false);
  }
}

事件触发

现在我们给一个按钮绑定 React 的 mousedown 事件,然后触发。

调用栈为:

链路很长,只说核心的。

首先 执行 dispatchEventForPluginEventSystem,这里用 batchedUpdates 批量更新策略执行了 dispatchEventsForPlugins。

function dispatchEventForPluginEventSystem(
  domEventName,
  eventSystemFlags,
  nativeEvent,
  targetInst,
  targetContainer,
) {
  batchedUpdates(() =>
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst,
      targetContainer,
    ),
  );
}

dispatchEventsForPlugins 方法下,做的事情:

  • 首先用 extractEvents 递归 Fiber 树,从目标节点往上找到根节点,取出 props 中所有的相同事件(这里是 onMousedown),加到 dispatchQueue 队列上;
  • 然后调用 processDispatchQueue 去按照一定顺序执行(冒泡阶段时顺序,捕获阶段时倒序)。
function dispatchEventsForPlugins(
  domEventName,
  eventSystemFlags,
  nativeEvent,
  targetInst,
  targetContainer,
) {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue = [];
  // 1. 找到所有需要执行的事件
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // 2. 按一定顺序执行
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

首先是 调用 extractEvents,该方法会调用不同的事件插件对象的 extractEvents 方法。

function extractEvents(...) {
  SimpleEventPlugin.extractEvents(...);

  if (shouldProcessPolyfillPlugins) {
    EnterLeaveEventPlugin.extractEvents(...);
    ChangeEventPlugin.extractEvents(...);
    SelectEventPlugin.extractEvents(...);
    BeforeInputEventPlugin.extractEvents(...);
  }
}

这里以 SimpleEventPlugin  事件插件对象的 extractEvents 方法为例,做了下面事情:

// 这个 extractEvents 是 SimpleEventPlugin 下的方法
function extractEvents(
  dispatchQueue,
  domEventName,
  targetInst,
  nativeEvent,
  nativeEventTarget,
  eventSystemFlags,
  targetContainer,
) {
  // 先默认一个合成对象构造函数
  let SyntheticEventCtor = SyntheticEvent;
  switch (domEventName) {
    // ...
    case 'mousedown':
      // 构造函数替换为 合成鼠标事件构造函数
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
  }

  // 是否为捕获阶段
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

  // 递归找到所有的 listeners
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent,
  );
  // 实例化合成事件对象,后面要传到 listener 里
  const event = new SyntheticEventCtor(
    reactName,
    reactEventType,
    null,
    nativeEvent,
    nativeEventTarget,
  );
  // 给队列加上 合成对象实例,和 listeners
  dispatchQueue.push({event, listeners});
}

dispatchQueue 队列的元素是一个对象,有 event 和 listeners 两个属性。event 是合成对象实例。listeners 是一个对象数组,由多个 listener 组成。

dispatchQueue 长下面这个样子:

然后就是将队列里的函数拿去执行。

processDispatchQueue 方法会将队列里的不同事件的 listeners 数组拿去执行。

export function processDispatchQueue(dispatchQueue, eventSystemFlags) {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    // 取出不同的事件
    const {event, listeners} = dispatchQueue[i];
    // 每个事件的 listeners 按一定顺序执行
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
}

processDispatchQueueItemsInOrder 是按一定顺序执行的,因为事件捕获和冒泡阶段的顺序是相反的,所以代码逻辑中会有两种执行方向。

一条链路中的 listener 拿到的都是同一个 event。执行 event.stopPropagation() 后,event.isPropagationStopped() 就一定会返回 true,然后链路就会被中断。

function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
  let previousInstance;
  if (inCapturePhase) {
    // 捕获阶段为逆序
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const { instance, currentTarget, listener } = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    // 冒泡阶段为顺序
    for (let i = 0; i < dispatchListeners.length; i++) {
      const { instance, currentTarget, listener } = dispatchListeners[i];
      // 是否中断冒泡
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

结尾

把源码细节丢掉,总结一下原理。

在 React 项目启动时,React 会在 ReactDOM 挂载的根节点上绑定事件,做事件委托,自己模拟浏览器的事件流,实现一套 React 事件流。

根节点绑定的通常是两个事件,一个用于模拟捕获阶段,一个模拟冒泡阶段。(但有些事件比较特别,是不能捕获冒和泡的,比如 scroll 事件,这种事件只会绑定一个事件模拟捕获阶段,且不支持事件委托)

用户触发了 React 事件,这里假设为 mousedown 的冒泡阶段。前面绑定的函数通过事件委托拿到了原生事件名,以及目标元素。基于它们,先创建一个合成事件对象,再从 fiber 树中不停往根节点找,将这些 fiberNode 的 props 的 onMousedown 放到一个队列中。

收集完毕后,我们根据当前是事件捕获还是冒泡阶段,选择方向去遍历同步执行。捕获阶段是从根节点到目标节点,冒泡则相反。另外,可以通过 event.stopPropagation() 阻止事件冒泡。

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

(0)

相关推荐

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

    目录 热身准备 明确几个概念 事件系统角色划分 事件注册 registerSimpleEvents registerEvents$2 registerEvents$1 registerEvents$3 registerEvents 事件监听 事件派发 合成事件 捕获和冒泡 总结 热身准备 明确几个概念 在React@17.0.3版本中: 所有事件都是委托在id = root的DOM元素中(网上很多说是在document中,17版本不是了): 在应用中所有节点的事件监听其实都是在id = root

  • React合成事件详解

    react合成事件指的是react用js模拟了一个Dom事件流.(fiber树模拟Dom树结构) 合成事件的事件流在fiber树中发生捕获和冒泡. 从点击输入框开始 当你点击input输入框,react在根节点(注1)监听到focus事件(注2)(注3). 如何从原生事件找到对应的虚拟Dom? 此时,react得到的信息只有原生事件对象(nativeEvent).react通过nativeEvent对应的Dom(eventTarget),沿着Dom树向上找到距离该eventTarget最近的被r

  • react合成事件与原生事件的相关理解

    1. 原生事件 原生事件就是js的原生事件,如通过document.addEventListener来设置的监听事件. 在react中即使有自己的一套事件机制(见下面合成事件),但有时候的业务场景我们仍然需要使用原生事件.比如我们封装一个Modal弹窗组件,需要在点击非弹窗区域时关掉弹窗,此时我们只能针对document进行原生点击事件监听. 由于原生事件需要绑定在真实DOM上,所以一般是在componentDidMount阶段或者组件/元素的ref的函数执行阶段进行绑定操作,并且注意要在com

  • React合成事件原理解析

    目录 事件介绍 什么是事件? 举个栗子 代码实现 React合成事件基础知识 什么是合成事件? 在React中事件的写法和原生事件写法的区别? 为什么会有合成事件? 合成事件机制简述 React合成事件实现原理 事件注册 事件触发-事件监听器做了什么 React中模拟冒泡和捕获 总结 事件介绍 什么是事件? 事件是在编程时系统内发生的动作或者发生的事情,而开发者可以某种方式对事件做出回应,而这里有几个先决条件 事件对象 给事件对象注册事件,当事件被触发后需要做什么 事件触发 举个栗子 在机场等待

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

    目录 事件绑定 事件触发 结尾 今天尝试学习 React 事件的源码实现. React 版本为 18.2.0 React 中的事件,是对原生事件的封装,叫做合成事件.抽象出一层合成事件,是为了做兼容,抹平不同浏览器之间的差异. 下面会从两个方面进行源码的解读: 事件绑定 事件触发 事件绑定 首先是 React 项目过程启动时,调用 listenToAllSupportedEvents 方法,做合成事件的绑定. // 对应方法 `ReactDOM.createRoot() function cre

  • React事件机制源码解析

    React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的. 本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关. 原理简述 React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身. 同时,React将事件分为3种类型--d

  • jQuery 1.9.1源码分析系列(十)事件系统之绑定事件

    事件绑定的方法有很多种,使用了jquery那么原理那种绑定方式(elem.click = function(){...}))就不太想推荐给大家了.最主要的原因是elem.click=fn这种方式只能绑定一个事件处理,多次绑定的只会保留最后一次绑定的结果. 下面给大家介绍jquery绑定事件的方式有哪些吧. 复制代码 代码如下: jQuery.fn.eventType([[data,] fn]) 比如eventType指的是事件类型,比如click: $("#chua").click(f

  • jQuery-1.9.1源码分析系列(十)事件系统之事件体系结构

    又是一个重磅功能点. 在分析源码之前分析一下体系结构,有助于源码理解.实际上在jQuery出现之前,Dean Edwards的跨浏览器AddEvent()设计做的已经比较优秀了:而且jQuery事件系统的设计思想也是基于该思想的,所以我们先分析一下Dean Edwards前辈的事件绑定. a. jQuery事件原型--Dean Edwards的跨浏览器AddEvent()设计 源码解读 //事件添加方法 function addEvent(element, type, handler) { //

  • jQuery-1.9.1源码分析系列(十)事件系统之事件包装

    在上篇文章给大家介绍了jQuery-1.9.1源码分析系列(十)事件系统之事件体系结构,本篇继续给大家介绍jquery1.9.1源码分析系列相关知识,具体内容请看下文吧. 首先需要明白,浏览器的原生事件是只读的,限制了jQuery对他的操作.举个简单的例子就能明白为什么jQuery非要构造一个新的事件对象. 在委托处理中,a节点委托b节点在a被click的时候执行fn函数.当事件冒泡到b节点,执行fn的时候上下文环境需要保证正确,是a节点执行了fn而非b节点.如何保证执行fn的上下文环境是a节点

  • Netty分布式源码分析监听读事件

    前文传送门:NioSocketChannel注册到selector 我们回到AbstractUnsafe的register0()方法: private void register0(ChannelPromise promise) { try { //省略代码 //做实际的注册 doRegister(); neverRegistered = false; registered = true; //触发事件 pipeline.invokeHandlerAddedIfNeeded(); safeSet

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • React源码分析之useCallback与useMemo及useContext详解

    目录 热身准备 初始化mount mountCallback 更新 update 使用场景 总结 热身准备 createContext Provider Consumer useContext 初始化mount&更新update 总结 热身准备 useCallback和useMemo是一样的东西,只是入参有所不同. useCallback缓存的是回调函数,如果依赖项没有更新,就会使用缓存的回调函数: useMemo缓存的是回调函数的return,如果依赖项没有更新,就会使用缓存的return:

  • React Context原理深入理解源码示例分析

    目录 正文 一.概念 二.使用 2.1.React.createContext 2.2.Context.Provider 2.3.React.useContext 2.4.Example 三.原理分析 3.1.createContext 函数实现 3.2. JSX 编译 3.3.消费组件 - useContext 函数实现 3.4.Context.Provider 在 Fiber 架构下的实现机制 3.5.小结 四.注意事项 五.对比 useSelector 正文 在 React 中提供了一种「

  • 浅谈bootstrap源码分析之tab(选项卡)

    实现tab选项卡的应用,此插件相对比较简单 源码文件: tab.js 实现原理 1.单击一个元素时,首先将原来高亮的元素取消 2.然后给被单击元素进行高亮 3.如果单击元素是下拉框中某个选项,则选中本身,还要选中下拉框 5.如果定义了动画,先执行动画,然后回调 源码分析: 1.Show方法,是在单击一个元素的时候触发,会触发如下四个事件 1.1.Hiden.bs.tab:隐藏上一个元素 1.2.Show.bs.tab:显示当前元素 1.3.Hideen.bs.tab:隐藏上一个元素完成 1.4.

随机推荐