React事件机制 - 源码概览(上)

某次被问到 React事件机制的问题,关于这一块我确实不怎么清楚,因为平时大部分工作都是用 Vue,对于 React的熟悉程度只限于会用,具体实现逻辑还真没专门学习过,但是总不能就说自己不清楚吧,好在我了解 Vue的事件机制,于是就把 Vue的事件机制说了一遍,最后再来一句“我觉得 React应该和 Vue的差不多”

后来我想了下应该没那么简单,于是网上搜了下相关文章,发现果然是被我想得太简单了,Vue通过编译模板,解析出事件指令,将事件和事件回调附加到 vnode tree上,在 patch过程中的创建阶段和更新阶段都会对这个 vnode tree进行处理,拿到每个 vnode上附加的事件信息,就可以调用原生 DOM API对相应事件进行注册或移除,流程还是比较清晰的,而React则是单独实现了一套事件机制

本文以 React v16.5.2 为基础进行源码分析

基本流程

react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:

/**
 * Summary of `ReactBrowserEventEmitter` event handling:
 *
 *  - Top-level delegation is used to ......
 * ......
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 *    React Core     .  General Purpose Event Plugin System
 */
复制代码

这段注释第一段文本内容被我省略掉了,其主要是在大概描述 React的事件机制,也就是这个文件中的代码要做的一些事情,大概意思就是说事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁,至于下面的结构图形,则是对事件机制的一个图形化描述

根据这段注释,大概可以提炼出以下几点内容:

  • React事件使用了事件委托的机制,一般事件委托的作用都是为了减少页面的注册事件数量,减少内存开销,优化浏览器性能,React这么做也是有这么一个目的,除此之外,也是为了能够更好的管理事件,实际上,React中所有的事件最后都是被委托到了 document这个顶级DOM
  • 既然所有的事件都被委托到了 document上,那么肯定有一套管理机制,所有的事件都是以一种先进先出的队列方式进行触发与回调
  • 既然都已经接管事件了,那么不对事件做些额外的事情未免有些浪费,于是 React中就存在了自己的 合成事件(SyntheticEvent),合成事件由对应的 EventPlugin负责合成,不同类型的事件由不同的 plugin合成,例如 SimpleEvent PluginTapEvent Plugin
  • 为了进一步提升事件的性能,使用了 EventPluginHub这个东西来负责合成事件对象的创建和销毁

下文均以下述这段代码为示例进行分析:

export default class MyBox extends React.Component {
  clickHandler(e) {
    console.log('click callback', e)
  }
  render() {
    return (
      <div className="box" onClick={this.clickHandler}>文本内容</div>
    )
  }
}
复制代码

事件注册

只看相关主体流程,其他诸如 vnode的创建等前提流程就不管了,从setInitialDOMProperties这个方法开始看起,这个方法主要用于遍历 ReactNodeprops对象,给最后将要真正渲染的真实 DOM对象设置一系列的属性,例如 styleclassautoFocus,也包括innerHTMLevent的处理等,示例中 .box元素的 props对象结构如下:

这个方法中有个 case,就是专门用于处理事件的:

// react-dom/src/client/ReactDOMComponent.js
else if (registrationNameModules.hasOwnProperty(propKey)) {
  if (nextProp != null) {
    if (true && typeof nextProp !== 'function') {
      warnForInvalidEventListener(propKey, nextProp);
    }
    // 处理事件类型的 props
    ensureListeningTo(rootContainerElement, propKey);
  }
}
复制代码

其中的 registrationNameModules这个变量,里面存在一大堆的属性,都是与 React的事件相关:

例子中的 onClick这个 props显然符合,所以可以执行 ensureListeningTo这个方法:

// react-dom/src/client/ReactDOMComponent.js
function ensureListeningTo(rootContainerElement, registrationName) {
  var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}
复制代码

这个方法中,首先判断了 rootContainerElement是不是一个 document或者 Fragment(文档片段节点),示例中传过来的是 .box这个 div,显然不是,所以 doc这个变量就被赋值为 rootContainerElement.ownerDocument,这个东西其实就是 .box所在的 document元素,把这个document传到下面的 listenTo里了,事件委托也就是在这里做的,所有的事件最终都会被委托到 document 或者 fragment上去,大部分情况下都是 document,然后这个 registrationName就是事件名称 onClick

接着开始执行 listenTo方法,这个方法其实就是注册事件的入口了,方法里面有这么一句:

// react-dom/src/events/ReactBrowserEventEmitter.js
var dependencies = registrationNameDependencies[registrationName];
复制代码

registrationName就是传过来的 onClick,而变量 registrationNameDependencies是一个存储了 React事件名与浏览器原生事件名对应的一个 Map,可以通过这个 map拿到相应的浏览器原生事件名,registrationNameDependencies结构如下:

可以看到,React是给事件名做了一些跨浏览器兼容事情的,比如传入 onChange事件,会自动对应上 blur change click focus等多种浏览器原生事件

接下来,遍历这个 dependencies数组,进入到以下 case

// react-dom/src/events/ReactBrowserEventEmitter.js
switch (dependency) {
  // 省略一些代码
  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.
    var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
    if (!isMediaEvent) {
      trapBubbledEvent(dependency, mountAt);
    }
    break;
}
复制代码

除了 scroll focus blur cancel close方法走 trapCapturedEvent方法,invalid submit reset方法不处理之外,剩下的事件类型全走default,执行 trapBubbledEvent这个方法,trapCapturedEventtrapBubbledEvent二者唯一的不同之处就在于,对于最终的合成事件,前者注册捕获阶段的事件监听器,而后者则注册冒泡阶段的事件监听器

由于大部分合成事件的代理注册的都是冒泡阶段的事件监听器,也就是委托到 document上注册的是冒泡阶段的事件监听器,所以就算你显示声明了一个捕获阶段的 React事件,例如 onClickCapture,此事件的响应也会晚于原生事件的捕获事件以及冒泡事件 实际上,所有原生事件的响应(无论是冒泡事件还是捕获事件),都将早于 React合成事件(SyntheticEvent),对原生事件调用 e.stopPropagation()将阻止对应 SyntheticEvent的响应,因为对应的事件根本无法到达document 这个事件委托层就被阻止掉了

二者区别不大,trapBubbledEvent用的最多,本示例也将执行这个方法,所以就跟着这个方法看下去:

// react-dom/src/events/EventListener.js
// 对于本示例来说,topLevelType就是 click,element就是 document
function trapBubbledEvent(topLevelType, element) {
  if (!element) {
    return null;
  }
  var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;

  addEventBubbleListener(element, getRawEventName(topLevelType),
  // Check if interactive and wrap in interactiveUpdates
  dispatch.bind(null, topLevelType));
}
复制代码

addEventBubbleListener这个方法接收三个参数,在本示例中,第一个参数 element其实就是 document元素,getRawEventName(topLevelType)就是 click事件,第三个参数的 dispatch就是 dispatchInteractiveEventdispatchInteractiveEvent其实最后还是会执行 dispatchEvent这个方法,只是在执行这个方法之前做了一些额外的事情,这里不需要关心,可以暂且认为二者是一样的

看下 addEventBubbleListener这个方法:

// react-dom/src/events/EventListener.js
export function addEventBubbleListener(
  element: Document | Element,
  eventType: string,
  listener: Function,
): void {
  element.addEventListener(eventType, listener, false);
}
复制代码

这个方法很简单,就是用 addEventListenerdocument注册了一个冒泡事件,listener这个事件的回调就是之前传入 dispatch.bind(null, topLevelType)

流程图如下:

事件分发

既然所有的事件都委托注册到了 document上,那么事件触发的时候,肯定需要一个事件分发的过程,来找到到底是哪个元素触发的事件,并执行相应的回调函数,需要注意的是,由于元素本身并没有注册任何事件,而是委托到了 document上,所以这个将被触发的事件是 React自带的合成事件,而非浏览器原生事件,但总之都是需要一个分发的过程的

在前面的 事件注册中已经提到过,注册到 document上的事件,对应的回调函数都会触发 dispatchEvent这个方法,进入这个方法:

// react-dom/src/events/ReactDOMEventListener.js
const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
复制代码

首先找到事件触发的 DOMReact Component,找真实 DOM比较好找,直接取事件回调的 event参数的 target | srcElement | window即可,然后这个 nativeEventTarget对象上挂在了一个以 __reactInternalInstance开头的属性,这个属性就是 internalInstanceKey,其值就是当前 React实例对应的 React Component

然后继续往下看:

try {
  // Event queue being processed in the same cycle allows
  // `preventDefault`.
  batchedUpdates(handleTopLevel, bookKeeping);
} finally {
  releaseTopLevelCallbackBookKeeping(bookKeeping);
}
复制代码

batchedUpdates,字面意思就是批处理更新,这里实际上就是把当前触发的事件放入了批处理队列中,其中,handleTopLevel是事件分发的核心所在:

// react-dom/src/events/ReactDOMEventListener.js
let targetInst = bookKeeping.targetInst;

// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
let ancestor = targetInst;
do {
  if (!ancestor) {
    bookKeeping.ancestors.push(ancestor);
    break;
  }
  const root = findRootContainerNode(ancestor);
  if (!root) {
    break;
  }
  bookKeeping.ancestors.push(ancestor);
  ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
复制代码

首先在事件回调之前,根据当前组件,向上遍历得到其所有的父组件,存储到 ancestors中,由于所有的事件都委托到了 document上,所以在事件触发后,无论是冒泡事件还是捕获事件,其在相关元素上的触发肯定是要有一个次序关系的,比如在子元素和父元素上都注册了一个鼠标点击冒泡事件,事件触发后,肯定是子元素的事件响应快于父元素,所以在事件队列里,子元素就要排在父元素前面,而在事件回调之前就要进行缓存,原因在代码的注释里也已经解释得很清楚了,大概意思就是事件回调可能会改变 DOM结构,所以要先遍历好组件层级关系,缓存起来

继续往下:

// react-dom/src/events/ReactDOMEventListener.js
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
  targetInst = bookKeeping.ancestors[i];
  runExtractedEventsInBatch(
    bookKeeping.topLevelType,
    targetInst,
    bookKeeping.nativeEvent,
    getEventTarget(bookKeeping.nativeEvent),
  );
}
复制代码

使用了一个 for循环来遍历这个 React Component及其所有的父组件,执行 runExtractedEventsInBatch方法,这里的遍历方法是从前往后遍历,前面说了,我们这里分析的是 trapBubbledEvent,也就是冒泡事件,所以这里对应到组件层级上就是由子元素到父元素,如果这里是分析 trapCapturedEvent,即捕获事件,那么这个从前往后的顺序就对应父元素到子元素了 提醒一点,无论是 trapBubbledEvent还是 trapCapturedEvent,这里都是针对 document元素而不是实际的元素,不要弄混了

至于循环中调用的 runExtractedEventsInBatch方法,其实就是事件执行的入口了

事件执行

runExtractedEventsInBatch这个方法中又调用了两个方法:extractEventsrunEventsInBatchextractEvents用于构造合成事件,runEventsInBatch用于批处理 extractEvents构造出的合成事件

构造合成事件

找到合适的合成事件的 plugin

先看 extractEvents

// packages/events/EventPluginHub.js
let events = null;
for (let i = 0; i < plugins.length; i++) {
  // Not every plugin in the ordering may be loaded at runtime.
  const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
  if (possiblePlugin) {
    const extractedEvents = possiblePlugin.extractEvents(
      topLevelType,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    if (extractedEvents) {
      events = accumulateInto(events, extractedEvents);
    }
  }
}
复制代码

首先遍历 plugins,这个 plugins就是所有事件合成 plugins的集合数组,一共 5种(v15.x版本是 7种),这些 plugins都位于 react-dom/src/events这个文件夹下,以单独文件的形式存在,文件名以 EventPlugin结尾的就是,它们是在 EventPluginHub初始化阶段注入进去的:

// react-dom/src/client/ReactDOMClientInjection.js
EventPluginHub.injection.injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});
复制代码

extractEvents方法里用了一个 for循环,把所有的 plugin全都执行了一遍,个人理解没这个必要,找到合适的 plugin执行完之后就可以直接 break掉了 比如对于本示例的 click事件来说,合适的 pluginSimpleEventPlugin,其他的 plugin就算是进入走了一遍也只是做了个无用功而已,因为执行完其他 plugin后得到的 extractedEvents都不满足 if (extractedEvents)这个条件,无法给 events这个变量赋值或者覆盖赋值,当然,也可能这段代码还有其他比较隐秘的作用吧

possiblePlugin.extractEvents 这一句就是调用相应 plugin的构造合成事件的方法,其他的 plugin就不展开分析了,针对本示例的 SimpleEventPlugin,来看下它的 extractEvents

// react-dom/src/events/SimpleEventPlugin.js
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
  return null;
}
复制代码

首先,看下 topLevelEventsToDispatchConfig这个对象中有没有 topLevelType这个属性,只要有,那么说明当前事件可以使用 SimpleEventPlugin构造合成事件,对于本示例来说,topLevelType就是 click,而topLevelEventsToDispatchConfig结构如下:

这些属性就是一些常见的事件名,显然 clicktopLevelEventsToDispatchConfig的一个属性名,符合条件,可以继续往下执行,下面紧跟着的是一个 switch...case的判断语句,对于本示例来说,将在下面这个 casebreak掉:

// react-dom/src/events/SimpleEventPlugin.js
case TOP_CLICK:
  // 省略了一些代码
  EventConstructor = SyntheticMouseEvent;
  break;
复制代码

SyntheticMouseEvent可以看做是 SimpleEventPlugin的一个具体的子 plugin,相当于是对 SimpleEventPlugin这个大概念的 plugin又细分了一层,除了 SyntheticMouseEvent之外还有 SyntheticWheelEventSyntheticClipboardEventSyntheticTouchEvent

从合成事件对象池中取对象

设置好具体的 EventConstructor后,继续往下执行:

// react-dom/src/events/SimpleEventPlugin.js
const event = EventConstructor.getPooled(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
);
accumulateTwoPhaseDispatches(event);
return event;
复制代码

getPooled就是从 event对象池中取出合成事件,这种操作是 React的一大亮点,将所有的事件缓存在对象池中,可以大大降低对象创建和销毁的时间,提升性能

getPooledEventConstructor上的一个方法,这个方法是在 EventConstructor初始化的时候挂上去的,但归根到底,这个方法是位于 SyntheticEvent这个对象上,流程示意图如下:

这个 getPooled其实就是 getPooledEvent,在 SyntheticEvent初始化的过程中就被设置好初始值了:

// packages/events/SyntheticEvent.js
addEventPoolingTo(SyntheticEvent);
// 省略部分代码
function addEventPoolingTo(EventConstructor) {
  EventConstructor.eventPool = [];
  EventConstructor.getPooled = getPooledEvent;
  EventConstructor.release = releasePooledEvent;
}
复制代码

那么看下 getPooledEvent

// packages/events/SyntheticEvent.js
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  const EventConstructor = this;
  if (EventConstructor.eventPool.length) {
    const instance = EventConstructor.eventPool.pop();
    EventConstructor.call(
      instance,
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeInst,
    );
    return instance;
  }
  return new EventConstructor(
    dispatchConfig,
    targetInst,
    nativeEvent,
    nativeInst,
  );
}
复制代码

首次触发事件的时候(在本示例中就是 click事件),EventConstructor.eventPool.length0,因为这个时候是第一次事件触发,对象池中没有对应的合成事件引用,所以需要初始化,后续再触发事件的时候,就无需 new了,而是走上面那个逻辑,直接从对象池中取,通过 EventConstructor.eventPool.pop();获取合成对象实例

这里先看下初始化的流程,会执行 new EventConstructor这一句,前面说了,这个东西可以看做是 SyntheticEvent的子类,或者是由 SyntheticEvent扩展而来的东西,怎么扩展的呢,实际上是使用了一个 extend方法:

const SyntheticMouseEvent = SyntheticUIEvent.extend({
  screenX: null,
  screenY: null,
  clientX: null,
  clientY: null,
  pageX: null,
  pageY: null,
  // 省略部分代码
})
复制代码

首先,SyntheticMouseEvent这个合成事件,有自己的一些属性,这些属性其实和浏览器原生的事件回调参数对象 event的属性没多大差别,都有对于当前事件的一些描述,甚至连属性名都一样,只不过相比于浏览器原生的事件回调参数对象 event来说,SyntheticMouseEvent 或者说 合成事件SyntheticEvent的属性是由 React主动生成,经过 React的内部处理,使得其上附加的描述属性完全符合 W3C的标准,因此在事件层面上具有跨浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,也具备stopPropagation()preventDefault()等方法

对于本示例中的点击事件回调方法来说:

clickHandler(e) {
  console.log('click callback', e)
}
复制代码

其中的 e其实就是 合成事件而非浏览器原生事件的 event,所以开发者无需考虑浏览器兼容性,只需要按照 w3c规范取值即可,如果需要访问原生的事件对象,可以通过 e.nativeEvent 获得

SyntheticUIEvent这个东西主要就是往 SyntheticMouseEvent上加一些额外的属性,这里不用关心,然后这个 SyntheticMouseEvent.extend又是由 SyntheticEvent扩展 (extend)来的,所以最终会 new SyntheticEvent

先看下 extend方法:

// packages/events/SyntheticEvent.js
SyntheticEvent.extend = function(Interface) {
  const Super = this;

  // 原型式继承
  const E = function() {};
  E.prototype = Super.prototype;
  const prototype = new E();
  // 构造函数继承
  function Class() {
    return Super.apply(this, arguments);
  }
  Object.assign(prototype, Class.prototype);
  Class.prototype = prototype;
  Class.prototype.constructor = Class;

  Class.Interface = Object.assign({}, Super.Interface, Interface);
  Class.extend = Super.extend;
  addEventPoolingTo(Class);

  return Class;
};
复制代码

先来了个经典的寄生组合式继承,这种寄生方法最为成熟,大多数库都是使用这种继承方法,React这里也用了它,让EventConstructor继承于 SyntheticEvent,获得 SyntheticEvent上的一些属性和方法,如前面所说的 eventPoolgetPooled

既然存在继承关系,那么 new EventConstructor这个子类,自然就会调用父类 SyntheticEventnew方法,也就是开始调用合成组件的构造器了,开始真正构造合成事件,主要就是将原生浏览器事件上的参数挂载到合成事件上,包括 clientXscreenYtimeStamp等事件属性, preventDefaultstopPropagation等事件方法,例如前面所说的通过 e.nativeEvent获得的原生事件就是在这个时候挂载上去的:

// packages/events/SyntheticEvent.js
this.nativeEvent = nativeEvent;
复制代码

挂载的属性都是经过 React处理过的,具备跨浏览器能力,同样,挂载的方法也和原生浏览器的事件方法有所不同,因为此时的事件附加在 document上的,所以调用一些事件方法,例如 e.stopPropagation()其实是针对 document元素调用的,跟原本期望的元素不是同一个,那么为了让合成事件的表现达到原生事件的效果,就需要对这些方法进行额外的处理

处理的方法也比较简单,就是加了一个标志位,例如,对于 stopPropagation来说, React对其进行了包装:

// packages/events/SyntheticEvent.js
stopPropagation: function() {
  const event = this.nativeEvent;
  if (!event) {
    return;
  }

  if (event.stopPropagation) {
    event.stopPropagation();
  } else if (typeof event.cancelBubble !== 'unknown') {
    // The ChangeEventPlugin registers a "propertychange" event for
    // IE. This event does not support bubbling or cancelling, and
    // any references to cancelBubble throw "Member not found".  A
    // typeof check of "unknown" circumvents this issue (and is also
    // IE specific).
    event.cancelBubble = true;
  }

  this.isPropagationStopped = functionThatReturnsTrue;
}
复制代码

首先就是拿到浏览器原生事件,然后调用对应的 stopPropagation方法,这里需要注意一下,这里的 event是由 document这个元素上的事件触发而生成的事件回调的参数对象,而非实际元素的事件回调的参数对象,说得明白点,就是给document上触发的事件,例如点击事件,调用了一下 e.stopPropagation,阻止事件继续往 document 或者 Fragment 的父级传播

// packages/events/SyntheticEvent.js
// 这个函数其实就是返回了一个 true,与此对应的,还有个函数名为 functionThatReturnsFalse的函数,用来返回 false
function functionThatReturnsTrue() {
  return true;
}
复制代码

关键在于 this.isPropagationStopped = functionThatReturnsTrue;这一句,相当于是设置了一个标志位,对于冒泡事件来说,当事件触发,由子元素往父元素逐级向上遍历,会按顺序执行每层元素对应的事件回调,但如果发现当前元素对应的合成事件上的 isPropagationStoppedfalse值,则遍历的循环将中断,也就是不再继续往上遍历,当前元素的所有父元素的合成事件就不会被触发,最终的效果,就和浏览器原生事件调用 e.stopPropagation()的效果是一样的

捕获事件的原理与此相同,只不过是由父级往子级遍历的罢了

这些事件方法(包括 stopPropagationpreventDefault等)一般都是在事件回调函数内调用,而事件的回调函数则是在后面的批处理操作中执行的

var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
复制代码

拿到所有与当前触发事件相关的元素实例和事件回调函数

上述一大堆都是从上述代码的第一句 getPooled为入口进去的,主要是为了得到合成事件,拿到基本的合成事件以后,开始对这个合成事件进行进一步的加工,也就是 accumulateTwoPhaseDispatches这个方法要做的事情,这个方法涉及到的流程比较多,画个图清晰点:

代码和调用的方法都比较琐碎,但目标很清晰,就是保存当前元素及其父元素上挂在的所有事件回调函数,包括捕获事件(captured)和冒泡事件(bubbled),保存到事件event_dispatchListeners属性上,并且将当前元素及其父元素的react实例(在 v16.x版本中,这里的实例是一个 FiberNode)保存到event_dispatchInstances属性上

拿到了所有与事件相关的元素实例以及事件的回调函数之后,就可以对合成事件进行批量处理了

剩余分析部分请参见文章 React事件机制 - 源码概览(下)

猜你喜欢

转载自juejin.im/post/5bd32493f265da0ae472cc8e