node都会 react_React 事件 | 1. React 中的事件委托

本文探讨React事件处理机制,特别是事件委托,如何减少内存消耗并提高性能。React将事件绑定到document,模仿事件委托,通过ReactEventListener进行事件分发。同时,文章介绍了React事件的三个类别及其优先级,并讨论了未委托的Form和Media事件原因。
摘要由CSDN通过智能技术生成

说到 React 的事件,也算是 React 的一个非常有亮点的优化,它在原生事件体系的基础上,单独实现了一套事件机制。想要了解这个机制,首先的了解下什么是事件委托以及事件委托的好处。

事件委托

假设有如下 html,我们想要在每个 li 上绑定 onClick 事件,最直观的做法当然就是给每个 li 分别添加事件,增加事件回调。这种做法当然没错,但是我们有一种更好的做法,那就是在 ul 上添加有个监听事件,由于事件的冒泡机制,事件就会冒泡到ul上,因为ul上有事件监听,所以事件就会触发。

Event 对象提供了一个属性叫 target,可以返回事件的目标节点,我们成为事件源,也就是说,target 就可以表示为触发当前的事件 dom,我们可以根据 dom 进行判断到底是哪个元素触发了事件,根据不同的元素,执行不同的回调方法。

这就是事件委托。

<ul id="operate">
 <li id="add">add</li>
 <li id="edit">edit</li>
 <li id="delete">delete</li>
</ul>

事件委托有以下优点。

  1. 减少事件注册,节省内存,能够提升整体性能。
  2. 简化了dom节点更新时,相应事件的更新(用过 jquery 的都知道,动态加入的元素,事件需要重新绑定)。

React 数据结构

React 并不是将 click 事件直接绑定在 dom 上面,而是采用事件冒泡的形式冒泡到 document 上面,这个思路借鉴了事件委托机制。所以,React 中所有的事件最后都是被委托到了 document这个顶级 DOM 上。

这个事件委托是在什么时候完成的呢?我们看如下示例。

<div onClick={this.handlerClick}></div>

这就是一个普通的 div,经过 React 的处理后,render 中的内容会变成以下数据结构。

{
    $$typeof: REACT_ELEMENT_TYPE,
    type:'div',
    key: 'key_name',
    ref: "ref_name",
    props: {
        class: "class_name",
        id: "id_name",
        onClick:fn
    }
     _owner: ReactCurrentOwner.current,
}

React 会根据这个数据结构来构造真实的 dom,如果对之前的内容还有印象,我们在创建 HostComponent,也就是原生节点的时候,会调用 ensureListeningTo(rootContainerElement, propKey) 这个方法。

function ensureListeningTo(
  rootContainerElement: Element | Node,
  registrationName: string,
): void {
  // 正常的元素的 nodeType 是 ELEMENT_NODE,也就是元素节点
  // DOCUMENT_NODE 表示整个文档(DOM 树的根节点)
  // DOCUMENT_FRAGMENT_NODE 代表轻量级的 Document 对象,能够容纳文档的某个部分
  const isDocumentOrFragment =
    rootContainerElement.nodeType === DOCUMENT_NODE ||
    rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;

  // 如果不是 DOCUMENT_NODE 或者是 DOCUMENT_FRAGMENT_NODE,则 doc 的值是 rootContainerElement.ownerDocument.
  // ownerDocument 属性返回的是元素的 Document 对象
  const doc = isDocumentOrFragment
    ? rootContainerElement
    : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}

ensureListeningTo 就是用来完成事件委托的,对于本例来说,rootContainerElement 表示项目挂载的根节点 div,registrationName 就是我们绑定事件的名称 onClick。div 的 nodeType 是ELEMENT_NODE,也就是元素节点,那么会通过该元素的 ownerDocument 属性获得文档的 Document 对象,然后将 onClick 事件挂载到 Document 对象上。

了解了这点后,我们先不看 listenTo 方法,回过头来了解下 React 事件的整理流程。

React 事件流程

react 源码中有一个注释很好的总结了 React 的事件流程(在注释中画流程图,很社会)。

react-domsrceventsReactBrowserEventEmitter.js

/**
 *
 * Overview of React and the event system:
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 */

从这个图可以看出,Dom 事件发生后,React 通过事件委托机制将大部分事件代理至 Document 层,ReactEventListener 就是负责给元素绑定事件的。ReactEventEmitter 暴露接口给 React 组件层用于添加事件订阅(对外暴露了 listenTo 等方法)。EventPluginHub 负责管理和注册各种插件。在事件分发时,调用插件来生成合成事件。 React 事件系统使用了插件机制来管理不同行为的事件。这些插件会处理自己感兴趣的事件类型,并生成合成事件对象。

比如 SimpleEventPlugin 负责处理一些比较通用的事件类型,如blur、focus、click、submit、touchMove、mouseMove、scroll、drag、load。

EnterLeaveEventPlugin 负责处理 mouseEnter/mouseLeave 和 pointerEnter/pointerLeave 这两类事件,单独处理的原因是这两类事件不支持冒泡。

TapEventPlugin 是为了解决移动端IOS 300ms 点击延迟,该插件增加了一个 onTouchTap 事件,这个事件触发后,会忽略300ms 后的 onClick 事件。

这里还需要了解的是,EventPluginHub 中处理的时间其实是合成事件 (SyntheticEvent),React 为什么要定义合成事件这个概念呢,有三点原因:

  1. 合成事件 (SyntheticEvent) 可以认为是浏览器原生事件跨浏览器的封装,相当于 React 帮我们做了浏览器的兼容性处理。
  2. React 想通过 SyntheticEvent 实现跨平台事件机制。
  3. 原生事件升级、改造,比如 React 的 onChange 事件,它为表单元素定义了统一的值变动事件,例如 blur、change、focus、input 等。

React 事件委托

了解了 React 大概的事件体系后,接着看之前的 ensureListeningTo 方法,该方法总调用了 listenTo 方法,这个方法就是 ReactEventEmitter 中的(对应着上面的图看会清晰很多)。

react-domsrceventsReactBrowserEventEmitter.js

export function listenTo(
  registrationName: string,
  mountAt: Document | Element | Node,
): void {
  // listeningSet 是 Set,存放所有绑定在 document 上的事件名称
  const listeningSet = getListeningSetForElement(mountAt);

  // registrationNameDependencies 是一个存储了 React 事件名与原生事件名映射的 Map
  const dependencies = registrationNameDependencies[registrationName];

  // trapCapturedEvent 表示注册捕获阶段的事件监听器
  // trapBubbledEvent 表示注册冒泡阶段的事件监听器
  for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    if (!listeningSet.has(dependency)) {
      switch (dependency) {
        case TOP_SCROLL:
          trapCapturedEvent(TOP_SCROLL, mountAt);
          break;
        case TOP_FOCUS:
        case TOP_BLUR:
          trapCapturedEvent(TOP_FOCUS, mountAt);
          trapCapturedEvent(TOP_BLUR, mountAt);
          // We set the flag for a single dependency later in this function,
          // but this ensures we mark both as attached rather than just one.
          listeningSet.add(TOP_BLUR);
          listeningSet.add(TOP_FOCUS);
          break;
        case TOP_CANCEL:
        case TOP_CLOSE:
          if (isEventSupported(getRawEventName(dependency))) {
            trapCapturedEvent(dependency, mountAt);
          }
          break;
        case TOP_INVALID:
        case TOP_SUBMIT:
        case TOP_RESET:
          // We listen to them on the target DOM elements.
          // Some of them bubble so we don't want them to fire twice.
          break;
        default:
          // By default, listen on the top level to all non-media events.
          // Media events don't bubble so adding the listener wouldn't do anything.
          const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
          if (!isMediaEvent) {
            trapBubbledEvent(dependency, mountAt);
          }
          break;
      }
      listeningSet.add(dependency);
    }
  }
}

我们看下 registrationNameDependencies 的数据结构。registrationNameDependencies 比较多,我们看些熟悉的。registrationNameDependencies 存放的是 React 事件名与原生事件名映射的 Map。可以很明显的看到,有些事件是合成事件,比如 onBeforeInput,onChange 等。onClick 事件就是原生的 click。这些值都是插件赋予的。

{
  ...
  onAbort: ["abort"]
  onBeforeInput: (4) ["compositionend", "keypress", "textInput", "paste"]
  onBlur: ["blur"]
  onChange: (8) ["blur", "change", "click", "focus", "input", "keydown", "keyup", "selectionchange"]
  onClick: ["click"]
  ...
}

对于依赖的原生事件,scroll blur focus cancel close 方法注册捕获阶段的事件监听器。invalid submit reset 事件不做处理。剩下的事件需要判断是否是媒体触发的,比如 video / audio 的 onplaying 事件,onprogress 事件, onratechange 事件等,这些媒体事件也不需要处理。

React 这么做的原因和事件有关,有些事件是不冒泡的,所以不能在冒泡阶段进行事件委托。具体情况请看我的另一篇文章“JavaScript那些不会冒泡的事件”,大家可以先去了解下这些不会冒泡的事件,回头看 React 的处理流程会比较清晰。

上述代码中处理了个特殊的情况,那就是 form 的 submit invalid,reset 事件,这几个事件没有委托给 Document 对象,如下所示。

case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
  // We listen to them on the target DOM elements.
  // Some of them bubble so we don't want them to fire twice.
  break;

为什么呢?原因是如果将 submit 事件绑定到 Form 上,该事件的监听方法会触发两次。当提交按钮被点击后,事件开始冒泡,当冒泡到 Form 的上的时候,由于 Form 存在 onSubmit 回调,会执行一次,然后向上冒泡到 Document,还会执行一次。所以 React 对于 Form 事件就不做处理了。

<form onSubmit={this.handelSubmit}>
    用户名:<input type="text" value={this.state.name} onChange={this.handelName} />
    <input type="submit" defaultValue="提交"/>
</form>

React 同样也没有处理媒体事件,和 Form 事件不同的是,媒体事件是没有冒泡过程的,所以只能在捕获阶段进行事件委托。

<audio onPlay={this.handelPlay}
        controls
        src={mp3} >
</audio>

经过测试后发现,媒体事件如果在捕获阶段进行事件委托,该事件的回调方法会被执行两次,和 Form 的表现是一致的,所以媒体事件没有委托。

除了上述的特殊事件,剩下的事件执行 trapBubbledEvent,也就是注册冒泡阶段的事件监听器,click 事件当然也处在这个行列。

trapBubbledEvent 和 trapCapturedEvent 大同小异,本例中触发的 click 事件属于冒泡事件,我们分析 trapBubbledEvent 方法。

react-domsrceventsReactDOMEventListener.js

export function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element | Node,
): void {
  // 第三个参数 false 代表冒泡,true 代表捕获
  trapEventForPluginEventSystem(element, topLevelType, false);
}

trapBubbledEvent 调用 trapEventForPluginEventSystem 方法

function trapEventForPluginEventSystem(
  element: Document | Element | Node,
  topLevelType: DOMTopLevelEventType,
  capture: boolean,
): void {
  let listener;

  // 判断事件的优先级,根据不同的优先级,对监听函数做不同的处理
  switch (getEventPriority(topLevelType)) {
    case DiscreteEvent:
      listener = dispatchDiscreteEvent.bind(
        null,
        topLevelType,
        PLUGIN_EVENT_SYSTEM,
      );
      break;
    case UserBlockingEvent:
      listener = dispatchUserBlockingUpdate.bind(
        null,
        topLevelType,
        PLUGIN_EVENT_SYSTEM,
      );
      break;
    case ContinuousEvent:
    default:
      listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
      break;
  }

  // event 的名称
  const rawEventName = getRawEventName(topLevelType);

  // 真正添加事件监听
  if (capture) {
    addEventCaptureListener(element, rawEventName, listener);
  } else {
    addEventBubbleListener(element, rawEventName, listener);
  }
}

React 中的事件分为 3 类。分别是 DiscreteEvent(离散事件),UserBlockingEvent(用户阻塞事件),ContinuousEvent(连续事件)。不同类型的事件代表了不同的优先级。

  1. DiscreteEvent:click,blur,focus,submit,tuchStart 等,优先级是 0。
  2. UserBlockingEvent:touchMove,mouseMove,scroll,drag,dragOver 等,这些事件会阻塞用户的交互,优先级是 1。
  3. ContinuousEvent:load,error,loadStart,abort,animationend 等,优先级是 2,这个优先级最高,不会被打断。

根据优先级的不同,监听函数做了不同的包装,我们先不管这里生成的监听函数和最初的监听方法有什么不同。最终我们会调用 addEventBubbleListener 方法。

addEventBubbleListener 就是 element.addEventListener,为目标添加事件监听函数。

export function addEventBubbleListener(
  element: Document | Element | Node,
  eventType: string,
  listener: Function,
): void {
  element.addEventListener(eventType, listener, false);
}

到此为止,一个事件 click 事件的监听方法就被添加到 Document 对象上了。小结一下:

  1. React 借鉴事件委托的方式将大部分事件委托给了 Document 对象。
  2. React 中的事件分为 3 类。分别是 DiscreteEvent(离散事件),UserBlockingEvent(用户阻塞事件),ContinuousEvent(连续事件)。不同类型的事件代表了不同的优先级。
  3. 事件委托需要区分捕获和冒泡,有些事件由于没有冒泡过程,只能在捕获阶段进行事件委托。
  4. 没有进行委托的事件是 Form 事件和 Media 事件,原因是这些事件委托后会触发两次回调函数。

建议先阅读我的另一篇文章JavaScript 中那些不会冒泡的事件,这样会更容易理解 React 对于事件的处理过程

如果您觉得有所收获,就麻烦点个赞吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值