React中的合成事件

React自己实现了一套高效的事件注册、存储、分发和重用逻辑,在DOM事件体系基础上做了很大改进,减少了内存消耗,简化了事件逻辑,并最大程度地解决了IE等浏览器的不兼容问题。

描述

React的合成事件SyntheticEvent实际上就是React自己在内部实现的一套事件处理机制,它是浏览器的原生事件的跨浏览器包装器,除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括stopPropagation()preventDefault(),合成事件与浏览器的原生事件不同,也不会直接映射到原生事件,也就是说通常不要使用addEventListener为已创建的DOM元素添加监听器,而应该直接使用React中定义的事件机制,而且在混用的情况下原生事件如果定义了阻止冒泡可能会阻止合成事件的执行,当然如果确实需要使用原生事件去处理需求,可以通过事件触发传递的SyntheticEvent对象的nativeEvent属性获得原生Event对象的引用,React中的事件有以下几个特点:

  • React上注册的事件最终会绑定在document这个DOM上,而不是React组件对应的DOM,通过这种方式减少内存开销,所有的事件都绑定在document上,其他节点没有绑定事件,实际上就是事件委托的。
  • React自身实现了一套事件冒泡机制,使用React实现的Event对象与原生Event对象不同,不能相互混用。
  • React通过队列的形式,从触发的组件向父组件回溯,然后调用他们JSX中定义的callback
  • React的合成事件SyntheticEvent与浏览器的原生事件不同,也不会直接映射到原生事件。
  • React通过对象池的形式管理合成事件对象的创建和销毁,减少了垃圾的生成和新对象内存的分配,提高了性能。

对于每个SyntheticEvent对象都包含以下属性:

boolean bubbles boolean cancelable DOMEventTarget currentTarget boolean defaultPrevented number eventPhase boolean isTrusted DOMEvent nativeEvent void preventDefault() boolean isDefaultPrevented() void stopPropagation() boolean isPropagationStopped() void persist() DOMEventTarget target number timeStamp string type

支持的合成事件一览,注意以下的事件处理函数在冒泡阶段被触发,如需注册捕获阶段的事件处理函数,则应为事件名添加Capture,例如处理捕获阶段的点击事件请使用onClickCapture,而不是onClick

<!-- 剪贴板事件 --> onCopy onCut onPaste <!-- 复合事件 --> onCompositionEnd onCompositionStart onCompositionUpdate <!-- 键盘事件 --> onKeyDown onKeyPress onKeyUp <!-- 焦点事件 --> onFocus onBlur <!-- 表单事件 --> onChange onInput onInvalid onReset onSubmit <!-- 通用事件 --> onError onLoad <!-- 鼠标事件 --> onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp <!-- 指针事件 --> onPointerDown onPointerMove onPointerUp onPointerCancel onGotPointerCapture onLostPointerCapture onPointerEnter onPointerLeave onPointerOver onPointerOut <!-- 选择事件 --> onSelect <!-- 触摸事件 --> onTouchCancel onTouchEnd onTouchMove onTouchStart <!-- UI 事件 --> onScroll <!-- 滚轮事件 --> onWheel <!-- 媒体事件 --> onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting <!-- 图像事件 --> onLoad onError <!-- 动画事件 --> onAnimationStart onAnimationEnd onAnimationIteration <!-- 过渡事件 --> onTransitionEnd <!-- 其他事件 --> onToggle <!-- https://zh-hans.reactjs.org/docs/events.html -->

示例

一个简单的示例,同时绑定在一个DOM上的原生事件与React事件,因为原生事件阻止冒泡而导致React事件无法执行,同时我们也可以看到React传递的event并不是原生Event对象的实例,而是React自行实现维护的一个event对象。

<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>React</title> </head> <body> <div id="root"></div> </body> <script src="https://unpkg.zhimg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.zhimg.com/react-dom@17/umd/react-dom.development.js"></script> <script src="https://unpkg.zhimg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> class ReactEvent extends React.PureComponent { componentDidMount(){ document.getElementById("btn-reactandnative").addEventListener("click", (e) => { console.log("原生事件执行", "handleNativeAndReact"); console.log("event instanceof Event:", e instanceof Event); e.stopPropagation(); // 阻止冒泡即会影响了React的事件执行 }); } handleNativeAndReact = (e) => { console.log("React事件执行", "handleNativeAndReact"); console.log("event instanceof Event:", e instanceof Event); } handleClick = (e) => { console.log("React事件执行", "handleClick"); console.log("event instanceof Event:", e instanceof Event); } render() { return ( <div className="pageIndex"> <button id="btn-confirm" onClick={this.handleClick}>React 事件</button> <button id="btn-reactandnative" onClick={this.handleNativeAndReact}>原生 + React 事件</button> </div> ) } } var vm = ReactDOM.render( <> <ReactEvent /> </>, document.getElementById("root") ); </script> </html>

React事件系统

简单来说,在挂载的时候,通过listenerBank把事件存起来了,触发的时候document进行dispatchEvent,找到触发事件的最深的一个节点,向上遍历拿到所有的callback放在eventQueue,根据事件类型构建event对象,遍历执行eventQueue,不简单点说,我们可以查看一下React对于事件处理的源码实现,commit id4ab6305TAGReact16.10.2,在React17不再往document上挂事件委托,而是挂到DOM容器上,目录结构都有了很大更改,我们还是依照React16,首先来看一下事件的处理流程。

/** * Summary of `ReactBrowserEventEmitter` event handling: * * - Top-level delegation is used to trap most native browser events. This * may only occur in the main thread and is the responsibility of * ReactDOMEventListener, which is injected and can therefore support * pluggable event sources. This is the only work that occurs in the main * thread. * * - We normalize and de-duplicate events to account for browser quirks. This * may be done in the worker thread. * * - Forward these native events (with the associated top-level type used to * trap it) to `EventPluginHub`, which in turn will ask plugins if they want * to extract any synthetic events. * * - The `EventPluginHub` will then process each event by annotating them with * "dispatches", a sequence of listeners and IDs that care about that event. * * - The `EventPluginHub` then dispatches the events. */ /** * React和事件系统概述: * * +------------+ . * | DOM | . * +------------+ . * | . * v . * +------------+ . * | ReactEvent | . * | Listener | . * +------------+ . +-----------+ * | . +--------+|SimpleEvent| * | . | |Plugin | * +-----|------+ . v +-----------+ * | | | . +--------------+ +------------+ * | +-----------.--->|EventPluginHub| | Event | * | | . | | +-----------+ | Propagators| * | ReactEvent | . | | |TapEvent | |------------| * | Emitter | . | |<---+|Plugin | |other plugin| * | | . | | +-----------+ | utilities | * | +-----------.--->| | +------------+ * | | | . +--------------+ * +-----|------+ . ^ +-----------+ * | . | |Enter/Leave| * + . +-------+|Plugin | * +-------------+ . +-----------+ * | application | . * |-------------| . * | | . * | | . * +-------------+ . * . */

packages\react-dom\src\events\ReactBrowserEventEmitter.js中就描述了上边的流程,并且还有相应的英文注释,使用google翻译一下,这个太概述了,所以还是需要详细描述一下,在事件处理之前,我们编写的JSX需要经过babel的编译,创建虚拟DOM,并处理组件props,拿到事件类型和回调fn等,之后便是事件注册、存储、合成、分发、执行阶段。

  • Top-level delegation用于捕获最原始的浏览器事件,它主要由ReactEventListener负责,ReactEventListener被注入后可以支持插件化的事件源,这一过程发生在主线程。
  • React对事件进行规范化和重复数据删除,以解决浏览器的问题,这可以在工作线程中完成。
  • 将这些本地事件(具有关联的顶级类型用来捕获它)转发到EventPluginHub,后者将询问插件是否要提取任何合成事件。
  • 然后EventPluginHub将通过为每个事件添加dispatches(引用该事件的侦听器和ID的序列)来对其进行注释来进行处理。
  • 再接着,EventPluginHub会调度分派事件。

事件注册

首先会调用setInitialDOMProperties()判断是否在registrationNameModules列表中,在的话便注册事件,列表包含了可以注册的事件。

// packages\react-dom\src\client\ReactDOMComponent.js line 308
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      if (__DEV__) {
        if (nextProp) {
          // Freeze the next style object so that we can assume it won't be
          // mutated. We have already warned for this in the past.
          Object.freeze(nextProp);
        }
      }
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      setValueForStyles(domElement, nextProp);
    }else if(/* ... */){
        // ...
    } else if (registrationNameModules.hasOwnProperty(propKey)) { // 对事件名进行合法性检验,只有合法的事件名才会被识别并进行事件绑定
      if (nextProp != null) {
        if (__DEV__ && typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }
        ensureListeningTo(rootContainerElement, propKey); // 开始注册事件
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

如果事件名合法而且是一个函数的时候,就会调用ensureListeningTo()方法注册事件。ensureListeningTo会判断rootContainerElement是否为document或是Fragment,如果是则直接传递给listenTo,如果不是则通过ownerDocument来获取其根节点,对于ownerDocument属性,定义是这样的,ownerDocument可返回某元素的根元素,在HTMLHTML文档本身是元素的根元素,所以可以说明其实大部分的事件都是注册在document上面的,之后便是调用listenTo方法实际注册。

// packages\react-dom\src\client\ReactDOMComponent.js line 272
function ensureListeningTo(
  rootContainerElement: Element | Node,
  registrationName: string,
): void {
  const isDocumentOrFragment =
    rootContainerElement.nodeType === DOCUMENT_NODE ||
    rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  const doc = isDocumentOrFragment
    ? rootContainerElement
    : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}

listenTo()方法中比较重要的就是registrationNameDependencies的概念,对于不同的事件,React会同时绑定多个事件来达到统一的效果。此外listenTo()方法还默认将事件通过trapBubbledEvent绑定,将onBluronFocusonScroll等事件通过trapCapturedEvent绑定,因为这些事件没有冒泡行为,invalidsubmitreset事件以及媒体等事件绑定到当前DOM上。

// packages\react-dom\src\events\ReactBrowserEventEmitter.js line 128
export function listenTo(
  registrationName: string, // 事件的名称,即为上面的propKey(如onClick)
  mountAt: Document | Element | Node, // 事件注册的目标容器
): void {
  // 获取目标容器已经挂载的事件列表对象,如果没有则初始化为空对象
  const listeningSet = getListeningSetForElement(mountAt);
  // 获取对应事件的依赖事件,比如onChange会依赖TOP_INPUT、TOP_FOCUS等一系列事件
  const dependencies = registrationNameDependencies[registrationName];
    
   // 遍历所有的依赖,并挨个进行绑定
  for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    listenToTopLevel(dependency, mountAt, listeningSet);
  }
}

export function listenToTopLevel(
  topLevelType: DOMTopLevelEventType,
  mountAt: Document | Element | Node,
  listeningSet: Set<DOMTopLevelEventType | string>,
): void {
  if (!listeningSet.has(topLevelType)) {
    // 针对不同的事件来判断使用事件捕获还是事件冒泡
    switch (topLevelType) {
      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:
        // getRawEventName会返回真实的事件名称,比如onChange => onchange
        if (isEventSupported(getRawEventName(topLevelType))) {
          trapCapturedEvent(topLevelType, 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:
        // 默认将除了媒体事件之外的所有事件都注册冒泡事件
        // 因为媒体事件不会冒泡,所以注册冒泡事件毫无意义
        const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
        if (!isMediaEvent) {
          trapBubbledEvent(topLevelType, mountAt);
        }
        break;
    }
    // 表示目标容器已经注册了该事件
    listeningSet.add(topLevelType);
  }
}

之后就是熟知的对事件的绑定,以事件冒泡trapBubbledEvent()为例来描述处理流程,可以看到其调用了trapEventForPluginEventSystem方法。

// packages\react-dom\src\events\ReactDOMEventListener.js line 203
export function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element | Node,
): void {
  trapEventForPluginEventSystem(element, topLevelType, false);
}

可以看到React将事件分成了三类,优先级由低到高:

  • DiscreteEvent离散事件,例如blurfocusclicksubmittouchStart,这些事件都是离散触发的。
  • UserBlockingEvent用户阻塞事件,例如touchMovemouseMovescrolldragdragOver等等,这些事件会阻塞用户的交互。
  • ContinuousEvent连续事件,例如loaderrorloadStartabortanimationEnd,这个优先级最高,也就是说它们应该是立即同步执行的,这就是Continuous的意义,是持续地执行,不能被打断。

此外React将事件系统用到了Fiber架构里,Fiber中将任务分成了5大类,对应不同的优先级,那么三大类的事件系统和五大类的Fiber任务系统的对应关系如下。

  • Immediate: 此类任务会同步执行,或者说马上执行且不能中断,ContinuousEvent便属于此类。
  • UserBlocking: 此类任务一般是用户交互的结果,需要及时得到反馈,DiscreteEventUserBlockingEvent都属于此类。
  • Normal: 此类任务是应对那些不需要立即感受到反馈的任务,比如网络请求。
  • Low: 此类任务可以延后处理,但最终应该得到执行,例如分析通知。
  • Idle: 此类任务的定义为没有必要做的任务。

回到trapEventForPluginEventSystem,实际上在这三类事件,他们最终都会有统一的触发函数dispatchEvent,只不过在dispatch之前会需要进行一些特殊的处理。

// packages\react-dom\src\events\ReactDOMEventListener.js line 256
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:
      // 统一的分发函数 dispatchEvent
      listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
      break;
  }

  const rawEventName = getRawEventName(topLevelType);
  if (capture) {
    // 注册捕获事件
    addEventCaptureListener(element, rawEventName, listener);
  } else {
    // 注册冒泡事件
    addEventBubbleListener(element, rawEventName, listener);
  }
}

到达最终的事件注册,实际上就是在document上注册了各种事件。

// packages\react-dom\src\events\EventListener.js line 10
export function addEventBubbleListener(
  element: Document | Element | Node,
  eventType: string,
  listener: Function,
): void {
  element.addEventListener(eventType, listener, false);
}

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

export function addEventCaptureListenerWithPassiveFlag(
  element: Document | Element | Node,
  eventType: string,
  listener: Function,
  passive: boolean,
): void {
  element.addEventListener(eventType, listener, {
    capture: true,
    passive,
  });
}

事件存储

让我们回到上边的listenToTopLevel方法中的listeningSet.add(topLevelType),即是将事件添加到注册到事件列表对象中,即将DOM节点和对应的事件保存到Weak Map对象中,具体来说就是DOM节点作为键名,事件对象的Set作为键值,这里的数据集合有自己的名字叫做EventPluginHub,当然在这里最理想的情况会是使用WeakMap进行存储,不支持则使用Map对象,使用WeakMap主要是考虑到WeakMaps保持了对键名所引用的对象的弱引用,不用担心内存泄漏问题,WeakMaps应用的典型场合就是DOM节点作为键名。

// packages\react-dom\src\events\ReactBrowserEventEmitter.js line 88
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const elementListeningSets:
  | WeakMap
  | Map<
      Document | Element | Node,
      Set<DOMTopLevelEventType | string>,
    > = new PossiblyWeakMap();

export function getListeningSetForElement(
  element: Document | Element | Node,
): Set<DOMTopLevelEventType | string> {
  let listeningSet = elementListeningSets.get(element);
  if (listeningSet === undefined) {
    listeningSet = new Set();
    elementListeningSets.set(element, listeningSet);
  }
  return listeningSet;
}

事件合成

首先来看看handleTopLevel的逻辑,handleTopLevel主要是缓存祖先元素,避免事件触发后找不到祖先元素报错,接下来就进入runExtractedPluginEventsInBatch方法。

// packages\react-dom\src\events\ReactDOMEventListener.js line 151
function handleTopLevel(bookKeeping: BookKeepingInstance) {
  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) {
      const ancestors = bookKeeping.ancestors;
      ((ancestors: any): Array<Fiber | null>).push(ancestor);
      break;
    }
    const root = findRootContainerNode(ancestor);
    if (!root) {
      break;
    }
    const tag = ancestor.tag;
    if (tag === HostComponent || tag === HostText) {
      bookKeeping.ancestors.push(ancestor);
    }
    ancestor = getClosestInstanceFromNode(root);
  } while (ancestor);

  for (let i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    const eventTarget = getEventTarget(bookKeeping.nativeEvent);
    const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
    const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);

    runExtractedPluginEventsInBatch(
      topLevelType,
      targetInst,
      nativeEvent,
      eventTarget,
      bookKeeping.eventSystemFlags,
    );
  }
}

runExtractedPluginEventsInBatchextractPluginEvents用于通过不同的插件合成事件events,而runEventsInBatch则是完成事件的触发。

// packages\legacy-events\EventPluginHub.js line 160
export function runExtractedPluginEventsInBatch(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
  eventSystemFlags: EventSystemFlags,
) {
  const events = extractPluginEvents(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
  );
  runEventsInBatch(events);
}

extractPluginEvents中遍历所有插件的extractEvents方法合成事件,如果这个插件适合于这个events则返回它,否则返回null。默认的有5种插件SimpleEventPluginEnterLeaveEventPluginChangeEventPluginSelectEventPluginBeforeInputEventPlugin

// packages\legacy-events\EventPluginHub.js line 133
function extractPluginEvents(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
  eventSystemFlags: EventSystemFlags,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
  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,
        eventSystemFlags,
      );
      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }
  return events;
}

不同的事件类型会有不同的合成事件基类,然后再通过EventConstructor.getPooled生成事件,accumulateTwoPhaseDispatches用于获取事件回调函数,最终调的是getListener方法。
为了避免频繁创建和释放事件对象导致性能损耗(对象创建和垃圾回收),React使用一个事件池来负责管理事件对象(在React17中不再使用事件池机制),使用完的事件对象会放回池中,以备后续的复用,也就意味着事件处理器同步执行完后,SyntheticEvent属性就会马上被回收,不能访问了,也就是事件中的e不能用了,如果要用的话,可以通过一下两种方式:

  • 使用e.persist(),告诉React不要回收对象池,在React17依旧可以调用只是没有实际作用。
  • 使用e. nativeEvent,因为它是持久引用的。

事件分发

事件分发就是遍历找到当前元素及父元素所有绑定的事件,将所有的事件放到event._dispachListeners队列中,以备后续的执行。

// packages\legacy-events\EventPropagators.js line 47
function accumulateDirectionalDispatches(inst, phase, event) {
  if (__DEV__) {
    warningWithoutStack(inst, 'Dispatching inst must not be null');
  }
  const listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    // 将提取到的绑定添加到_dispatchListeners中
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

事件执行

执行事件队列用到的方法是runEventsInBatch,遍历执行executeDispatchesInOrder方法,通过executeDispatch执行调度,最终执行回调函数是通过invokeGuardedCallbackAndCatchFirstError方法。

// packages\legacy-events\EventBatching.js line 42
export function runEventsInBatch(
  events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }

  // Set `eventQueue` to null before processing it so that we can tell if more
  // events get enqueued while processing.
  const processingEventQueue = eventQueue;
  eventQueue = null;

  if (!processingEventQueue) {
    return;
  }

  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
  invariant(
    !eventQueue,
    'processEventQueue(): Additional events were enqueued while processing ' +
      'an event queue. Support for this has not yet been implemented.',
  );
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}

// packages\legacy-events\EventPluginUtils.js line 76
export function executeDispatchesInOrder(event) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;
  if (__DEV__) {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

// packages\legacy-events\EventPluginUtils.js line 66
export function executeDispatch(event, listener, inst) {
  const type = event.type || 'unknown-event';
  event.currentTarget = getNodeFromInstance(inst);
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

// packages\shared\ReactErrorUtils.js line 67
export function invokeGuardedCallbackAndCatchFirstError<
  A,
  B,
  C,
  D,
  E,
  F,
  Context,
>(
  name: string | null,
  func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
  context: Context,
  a: A,
  b: B,
  c: C,
  d: D,
  e: E,
  f: F,
): void {
  invokeGuardedCallback.apply(this, arguments);
  if (hasError) {
    const error = clearCaughtError();
    if (!hasRethrowError) {
      hasRethrowError = true;
      rethrowError = error;
    }
  }
}

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://zhuanlan.zhihu.com/p/53961511 https://zhuanlan.zhihu.com/p/25883536 https://zhuanlan.zhihu.com/p/140791931 https://www.jianshu.com/p/8d8f9aa4b033 https://toutiao.io/posts/28of14w/preview https://juejin.cn/post/6844903988794671117 https://segmentfault.com/a/1190000015142568 https://zh-hans.reactjs.org/docs/events.html https://github.com/UNDERCOVERj/tech-blog/issues/13 https://blog.csdn.net/kyooo0/article/details/111829693