本文内容基于 React 17.0.0,主要在 legacy mode 下针对 React-DOM 来进行分析

引言

React 的合成事件系统一直是 React 的标志性特性之一,其效果是在开发者和真实事件之间加了一层中间层,面对开发者可以输出符合其设计意图的 API,面对原始事件它可以进行挟持和加工。虚拟 DOM 也采用了类似的分层设计,在开发者和原始 DOM 之间加了一层抽象内容。这类抽象设计方式十分值得去深入学习,并且合成事件系统是了解 React 原理不可或缺的一步,故有了本篇文章。

如有错误,还请不吝斧正。

一致性

为了方便理解并防止分歧,有必要对本文内一些可能混淆的概念进行强调并释义。
请注意,释义仅对本文负责,并不一定适用于本文以外的内容,请注意甄别。

名词表

名词名称 释义
真实事件 泛指由开发者写在组件里的事件,试图挂载在 DOM 上的事件,也可理解为真实的事件。
原生事件 泛指浏览器里存在并会发生的 UI Events,比如 ‘click’、’cancel’ 等。
合成事件对象 由 React 进行封装过的 Event 对象
合成事件系统 泛指整个由 React 来处理事件机制的系统。
根 DOM 节点 并非指 Document 节点,而是 React 应用挂载的 DOM 节点。
事件代理 又称事件委托,与事件委托无异,笔者觉着在本文中“代理”这个措辞更合适所以便用了,故特此说明 : )

合成事件系统的目的

由于没有找到太官方的答案,所以只能提供一些相对主观的答案:

性能优化:使用事件代理统一接收原生事件的触发,从而可以使得真实 DOM 上不用绑定事件。(但对应的,可能会触发过多的无意义的事件收集。)
分层设计:解决跨平台问题。
合成事件对象:抹平浏览器差异。
挟持事件触发:原生事件的处理都是先通过 React 再交给真实事件,这样 React 可以知道触发了什么原生事件,是什么原生事件调用了对应的真实事件,真实事件内有关 React 相关状态的改动是在什么事件里触发的。

现版本 React 合成事件系统有一个不可替代的原因是,React 需要知道更新是由什么原生事件触发的。
React 挟持事件触发可以知道用户触发了什么事件,是通过什么原生事件调用的真实事件。这样可以通过对原生事件的优先级定义进而确定真实事件的优先级,再进而可以确定真实事件内触发的更新是什么优先级,最终可以决定对应的更新应该在什么时机更新。
**

初始化合成事件系统

初始化部分的工作内容相对简单,核心目的就是生成一些静态变量,方便后面使用。
下面会简单介绍一下重要的静态变量,并不会对其初始化过程进行研究,因为源码为了这点小事写的非常绕。

入口程序在 react-dom/src/events/DOMPluginEventSystem.js#L89-L93,有兴趣的可自行 debug。
这五个 EventPlugin 关系可以这么理解,SimpleEventPlugin 是合成事件系统的基本功能实现,而其他的几个 EventPlugin 只不过是它的 polyfill。

  1. SimpleEventPlugin.registerEvents();
  2. EnterLeaveEventPlugin.registerEvents();
  3. ChangeEventPlugin.registerEvents();
  4. SelectEventPlugin.registerEvents();
  5. BeforeInputEventPlugin.registerEvents();

eventPriorities

原生事件及其优先级的映射

  1. {
  2. "cancel": 0,
  3. // ...
  4. "drag": 1,
  5. // ...
  6. "abort": 2,
  7. // ...
  8. }

React 对原生事件的优先级定义主要有三类

  1. export const DiscreteEvent: EventPriority = 0; // 离散事件,cancel、click、mousedown 这类单点触发不持续的事件,优先级最低
  2. export const UserBlockingEvent: EventPriority = 1; // 用户阻塞事件,drag、mousemove、wheel 这类持续触发的事件,优先级相对较高
  3. export const ContinuousEvent: EventPriority = 2; // 连续事件,load、error、waiting 这类大多与媒体相关的事件为主的事件需要及时响应,所以优先级最高

topLevelEventsToReactNames

原生事件和合成事件的映射

  1. {
  2. "cancel": "onCancel",
  3. // ...
  4. "pointercancel": "onPointerCancel",
  5. // ...
  6. "waiting": "onWaiting"
  7. }

registrationNameDependencies

合成事件和其依赖的原生事件集合的映射

  1. {
  2. "onCancel": ["cancel"],
  3. "onCancelCapture": ["cancel"],
  4. // ...
  5. "onChange": ["change", "click", "focusin","focusout", "input", "keydown", "keyup", "selectionchange"],
  6. "onCancelCapture": ["change", "click", "focusin","focusout", "input", "keydown", "keyup", "selectionchange"],
  7. "onSelect": ["focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange"],
  8. "onSelectCapture": ["focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange"],
  9. // ...
  10. }

其他

除此之外,还有些不全是动态生成的变量或常量也简单介绍一下:

变量 数据类型 意义
allNativeEvents Set 所有有意义的原生事件名称集合
nonDelegatedEvents Set 不需要在冒泡阶段进行事件代理(委托)的原生事件名称集合

注册事件代理

17.x 相较于 16.x 发生了较大改变,请注意甄别。

React 采用了事件代理去捕获浏览器发生的原生事件,接着会利用原生事件里的 Event 对象去收集真实事件,然后调用真实事件。
收集事件我们后面会讲,我们先关注前半部分“事件代理”,它是在什么阶段做的,怎么做的。

在 17.x 版本中,创建 ReactRoot 阶段便会调用 listenToAllSupportedEvents 函数,并在所有可以监听的原生事件上添加监听事件。

调用链如下图所示:
image.png

listenToAllSupportedEvents

  1. export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  2. // 入参 rootContainerElement 由创建 ReactRoot 的函数传入,其内容为 React 应用的根 DOM 节点。
  3. // enableEagerRootListeners 为固定不变的标识常量,常为 true,可忽略。
  4. // 其意义是指“尽早的在所有原生事件上添加监听器”这一特性是否开启,与之相对的在 16.x 版本中监听器会在较晚的时机按需添加。
  5. if (enableEagerRootListeners) {
  6. // listeningMarker 是一个由固定字符加随机字符组成的标识,用于标识节点是否已经以 react 的方式在所有原生事件上添加监听事件,
  7. // 如果已经添加过,则直接跳过,节省一些不必要的工作
  8. if ((rootContainerElement: any)[listeningMarker]) {
  9. return;
  10. }
  11. // 添加标识
  12. (rootContainerElement: any)[listeningMarker] = true;
  13. // 遍历所有原生事件
  14. // 除了不需要在冒泡阶段添加事件代理的原生事件,仅在捕获阶段添加事件代理
  15. // 其余的事件都需要在捕获、冒泡阶段添加代理事件
  16. allNativeEvents.forEach(domEventName => {
  17. if (!nonDelegatedEvents.has(domEventName)) {
  18. listenToNativeEvent(
  19. domEventName,
  20. false,
  21. ((rootContainerElement: any): Element),
  22. null,
  23. );
  24. }
  25. listenToNativeEvent(
  26. domEventName,
  27. true,
  28. ((rootContainerElement: any): Element),
  29. null,
  30. );
  31. });
  32. }
  33. }

listenToNativeEvent

  1. // 简化版本
  2. export function listenToNativeEvent(
  3. domEventName: DOMEventName,
  4. isCapturePhaseListener: boolean,
  5. rootContainerElement: EventTarget,
  6. targetElement: Element | null,
  7. eventSystemFlags?: EventSystemFlags = 0,
  8. ): void {
  9. let target = rootContainerElement;
  10. // ...
  11. // target 节点上存了一个 Set 类型的值,内部存储着已经添加监听器的原生事件名称,目的是为了防止重复添加监听器。
  12. const listenerSet = getEventListenerSet(target);
  13. // 效果:'cancel' -> 'cancel__capture' | 'cancel__bubble'
  14. // 获取将要放到 listenerSet 里的事件名称
  15. const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
  16. // 如果未绑定则绑定
  17. if (!listenerSet.has(listenerSetKey)) {
  18. // 在现阶段 eventSystemFlags 入参常为 0,所以可以理解为,
  19. // 只要是在捕获阶段添加监听器的添加过程中,eventSystemFlags = IS_CAPTURE_PHASE = 1 << 2。
  20. if (isCapturePhaseListener) {
  21. eventSystemFlags |= IS_CAPTURE_PHASE;
  22. }
  23. addTrappedEventListener(
  24. target,
  25. domEventName,
  26. eventSystemFlags,
  27. isCapturePhaseListener,
  28. );
  29. // 添加至 listenerSet
  30. listenerSet.add(listenerSetKey);
  31. }
  32. }

addTrappedEventListener

  1. // 简化版本
  2. function addTrappedEventListener(
  3. targetContainer: EventTarget,
  4. domEventName: DOMEventName,
  5. eventSystemFlags: EventSystemFlags,
  6. isCapturePhaseListener: boolean,
  7. isDeferredListenerForLegacyFBSupport?: boolean,
  8. ) {
  9. // 创建带有优先级的事件监听器,具体内容后面概述
  10. let listener = createEventListenerWrapperWithPriority(
  11. targetContainer,
  12. domEventName,
  13. eventSystemFlags,
  14. );
  15. // ...
  16. let unsubscribeListener;
  17. // 在原生事件上添加不同阶段的事件监听器
  18. if (isCapturePhaseListener) {
  19. // ...
  20. unsubscribeListener = addEventCaptureListener(
  21. targetContainer,
  22. domEventName,
  23. listener,
  24. );
  25. } else {
  26. // ...
  27. unsubscribeListener = addEventBubbleListener(
  28. targetContainer,
  29. domEventName,
  30. listener,
  31. );
  32. }
  33. }

createEventListenerWrapperWithPriority(创建带优先级的监听器)

  1. export function createEventListenerWrapperWithPriority(
  2. targetContainer: EventTarget,
  3. domEventName: DOMEventName,
  4. eventSystemFlags: EventSystemFlags,
  5. ): Function {
  6. // 从前文提到的 eventPriorities 中获取当前原生事件的优先级
  7. const eventPriority = getEventPriorityForPluginSystem(domEventName);
  8. let listenerWrapper;
  9. // 根据不同的优先级提供不同的监听函数
  10. switch (eventPriority) {
  11. case DiscreteEvent:
  12. listenerWrapper = dispatchDiscreteEvent;
  13. break;
  14. case UserBlockingEvent:
  15. listenerWrapper = dispatchUserBlockingUpdate;
  16. break;
  17. case ContinuousEvent:
  18. default:
  19. listenerWrapper = dispatchEvent;
  20. break;
  21. }
  22. // 三类监听器的入参其实一样,其函数签名均为:
  23. // (domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent) => void
  24. // 前三个参数由当前函数提供,最后一个参数便是原生监听器会拥有的唯一入参 Event 对象
  25. return listenerWrapper.bind(
  26. null,
  27. domEventName,
  28. eventSystemFlags,
  29. targetContainer,
  30. );
  31. }

addEventCaptureListener/addEventBubbleListener(挂载监听器)

不赘述内容了

  1. export function addEventBubbleListener(
  2. target: EventTarget,
  3. eventType: string,
  4. listener: Function,
  5. ): Function {
  6. target.addEventListener(eventType, listener, false);
  7. return listener;
  8. }
  9. export function addEventCaptureListener(
  10. target: EventTarget,
  11. eventType: string,
  12. listener: Function,
  13. ): Function {
  14. target.addEventListener(eventType, listener, true);
  15. return listener;
  16. }

小结

最终在根 DOM 节点上,每个原生事件都绑定了其对应优先级所对应的 监听器
image.png

触发事件伊始

到此,文章之前的内容都可以理解为合成事件系统的准备工作,直到页面渲染完后成合成事件系统基本没有其他事了。
正常渲染完成后,当浏览器发生了原生事件的调用,合成事件系统才会开始工作👷‍♂️,前文提到的监听器会接收浏览器发生的事件,然后向下传递信息,收集相关事件,模拟浏览器的触发流程并达成我们预期的触发效果。
那么问题来了,React 的合成事件系统如何实现的呢?

效果总览

在讲解之前先给大家一个整体的概念,留个印象就好,后面讲解的时候可以对照着看。
测试 Demo 如下:
image.png
编写了三个事件 onClickonDragonPlaying ,分别对应了合成事件系统里三种优先级的事件,分别对触发对应的监听器。

其各个事件调用后的调用栈从上至下如下:
call stack.png
我们就着重研究每个调用栈的前半部分,弄明白 React 合成事件系统是如何运作的。
后半部分的 React 更新不在本文范围内,故不细讲。

监听器入口

根据调用栈以及上文的内容我们可以知道,当我们在页面点击按钮之后。以 onClick 为例子,率先触发的一定是挂载在根 DOM 节点上的 click 事件的监听器,也就是 dispatchDiscreteEvent
复习一下,在前文“注册时间代理-createEventListenerWrapperWithPriority”小节中提到,React 会根据不同的优先级提供不同的监听器,监听器共三种,分别是:

先简单说说这三个监听器的异同,
首先,这三个监听器的目的是相同的,最终目的都是进行事件收集、事件调用。具体代码层面,从调用栈可以知道,都会调用 dispatchEvent (第三类监听器)这个函数。
但不同的是,监听器在调用 dispatchEvent 之前发生的事情不一样,连续事件或其他事件监听器(第三类监听器) 由于其优先级最高的原因所以是直接同步调用的,而另外两类不同。
所以我们现在需要明白的是,前两类监听器在调用 dispatchEvent 之前都做了什么,为什么这么做,再然后我们去着重关注一下 dispatchEvent 具体干了些啥就够了。

由于其内容复杂性的原因,前两个监听器,笔者会先讲第二类监听器“用户阻塞事件监听器”方便读者理解。

dispatchUserBlockingUpdate(用户阻塞事件监听器)

函数内容很简单,调用了一个函数 runWithPriority ,传入了 当前任务的优先级 想要执行的任务(函数)
runWithPriority 的将当前任务的优先级标记在全局静态变量中,方便内部的更新知道当前是在什么优先级的事件中执行的。

在此,也可以解释为何第三类监听器是直接调用 dispatchEvent,而没有进行任何副作用操作,因为它是优先级坠高的,直接同步调用即可。

  1. // 简化版本
  2. // 前三个参数是在注册事件代理的时候便传入的,
  3. // domEventName:对应原生事件名称
  4. // eventSystemFlags:本文范文内其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
  5. // container:应用根 DOM 节点
  6. // nativeEvent:原生监听器传入的 Event 对象
  7. function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
  8. runWithPriority(
  9. UserBlockingPriority,
  10. dispatchEvent.bind(
  11. null,
  12. domEventName,
  13. eventSystemFlags,
  14. container,
  15. nativeEvent,
  16. ),
  17. )
  18. }

dispatchDiscreteEvent(离散事件监听器)

第一类监听器相对于第二类监听就复杂点了,下述从上至下其部分调用链

  1. function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
  2. // flushDiscreteUpdatesIfNeeded 的作用是清除先前积攒的为执行的离散任务,包括但不限于之前触发的离散事件 和 useEffect 的回调,
  3. // 主要为了保证当前离散事件所对应的状态时最新的
  4. // 如果觉得头疼,可以假装这行不存在,影响不大
  5. flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
  6. // 新建一个离散更新
  7. // 提前讲解一下它的入参,后面四个参数实际上第一个函数参数的参数
  8. // 后面会这么调用,dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent),是不是很眼熟
  9. discreteUpdates(
  10. dispatchEvent,
  11. domEventName,
  12. eventSystemFlags,
  13. container,
  14. nativeEvent,
  15. );
  16. }
  17. export function discreteUpdates(fn, a, b, c, d) {
  18. // 标记当前正在事件处理过程中,并存储之前的状态
  19. const prevIsInsideEventHandler = isInsideEventHandler;
  20. isInsideEventHandler = true;
  21. try {
  22. // 当前函数只需要关注这一行就行了
  23. // 调用 Scheduler 里的离散更新函数
  24. return discreteUpdatesImpl(fn, a, b, c, d);
  25. } finally {
  26. // 如果之前就处于事件处理过程中,则继续完成
  27. isInsideEventHandler = prevIsInsideEventHandler;
  28. if (!isInsideEventHandler) {
  29. finishEventHandler();
  30. }
  31. }
  32. }
  33. // react-reconciler 中的离散更新函数
  34. // 会做两件事,第一件事是调用对应离散事件,第二件事更新离散事件中可能生成的更新(如果时机对)
  35. // 如果你注意了前文离散事件监听器的调用栈,你会发现这里的两件事分别是
  36. // 第一件事:合成事件系统对于真实事件的收集及真实事件的调用
  37. // 第二件事:对真实事件中可能生成的更新进行更新
  38. discreteUpdatesImpl = function discreteUpdates<A, B, C, D, R>(
  39. fn: (A, B, C) => R,
  40. a: A,
  41. b: B,
  42. c: C,
  43. d: D,
  44. ): R {
  45. // 添加当前执行上下文状态,用于判断当前处在什么情况下,比如 RenderContext 说明更新处于 render 阶段
  46. // 全部上下文类型 https://github.com/facebook/react/blob/v17.0.0/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L249-L256
  47. const prevExecutionContext = executionContext;
  48. executionContext |= DiscreteEventContext;
  49. try {
  50. // 这里就跟第二类监听就一模一样啦!
  51. return runWithPriority(
  52. UserBlockingSchedulerPriority,
  53. fn.bind(null, a, b, c, d),
  54. );
  55. } finally {
  56. // 回归之前上下文
  57. executionContext = prevExecutionContext;
  58. // 如果已经没有执行上下文,说明已经执行完了,则可以开始更新生成的更新
  59. if (executionContext === NoContext) {
  60. // Flush the immediate callbacks that were scheduled during this batch
  61. resetRenderTimer();
  62. flushSyncCallbackQueue();
  63. }
  64. }
  65. }

小结

合成事件系统.png

dispatchEvent 之后

通过前文的描述,相信你一定清楚了 dispatchEvent 的重要性,它是所有花里胡哨的监听器的终点,其包含了合成事件系统的核心功能。
读者可以先回顾一下前文展示的调用栈,以 onClick 为例:
绘图1.png
实际上 dispathEvent 中最核心的内容就是调用 dispatchEventsForPlugins,因为正是这个函数触发了 事件收集、事件执行
在此之前他会做一系列啰嗦的调度以及边缘情况的判断,对于主流程的参考价值不大,所以笔者打算跳过对这些内容的讲解,直奔主题,仅对其入参来源进行讲解。

dispatchEventsForPlugins

着重讲一下第四个入参 targetInst,他的数据类型是 Fiber 或者 null,通常它就是一个 Fiber,所以大家可以忽略 null 的可能性,防止增加心智负担。
以前面测试 Demo 中的点击事件为例,targetInst 就是我们点击的那个 <button /> 对应的 Fiber 节点。
那 React 是怎么获取到这个 Fiber 节点呢,发生地主要在 调用栈中出现过的 attemptToDispatchEvent 函数中,其中分两步走:

  1. 获取监听器中传进来的 Event 对象,并获取 Event.target 内的 DOM 节点,这个 DOM 节点实际上就是 <button />获取函数
  2. 获取存储在 DOM 节点上的 Fiber 节点,Fiber 节点实际上存在 DOM.['__reactFiber$' + randomKey] 的键值上。获取函数对应的赋值函数
    1. function dispatchEventsForPlugins(
    2. domEventName: DOMEventName, // 事件名称
    3. eventSystemFlags: EventSystemFlags, // 事件处理阶段,4 = 捕获阶段,0 = 冒泡阶段
    4. nativeEvent: AnyNativeEvent, // 监听器的原生入参 Event 对象
    5. targetInst: null | Fiber, // event.target 对应的 DOM 节点的 Fiber 节点
    6. targetContainer: EventTarget, // 根 DOM 节点
    7. ): void {
    8. // 这里也获取了一遍 event.target
    9. const nativeEventTarget = getEventTarget(nativeEvent);
    10. // 事件队列,收集到的事件都会存储到这
    11. const dispatchQueue: DispatchQueue = [];
    12. // 收集事件
    13. extractEvents(
    14. dispatchQueue,
    15. domEventName,
    16. targetInst,
    17. nativeEvent,
    18. nativeEventTarget,
    19. eventSystemFlags,
    20. targetContainer,
    21. );
    22. // 执行事件
    23. processDispatchQueue(dispatchQueue, eventSystemFlags);
    24. }

extractEvents(收集事件)

extractEvents 的内容其实很简单,按需调用几个 EventPluginextractEvents,这几个 extractEvents 的目的是一样的,只不过针对不同的事件可能会生成不同的事件。我们就以最核心的也是最关键的 SimpleEventPlugin.extractEvents 来讲解

  1. function extractEvents(
  2. dispatchQueue: DispatchQueue,
  3. domEventName: DOMEventName,
  4. targetInst: null | Fiber,
  5. nativeEvent: AnyNativeEvent,
  6. nativeEventTarget: null | EventTarget,
  7. eventSystemFlags: EventSystemFlags,
  8. targetContainer: EventTarget,
  9. ): void {
  10. // 根据原生事件名称获取合成事件名称
  11. // 效果: onClick = topLevelEventsToReactNames.get('click')
  12. const reactName = topLevelEventsToReactNames.get(domEventName);
  13. if (reactName === undefined) {
  14. return;
  15. }
  16. // 默认合成函数的构造函数
  17. let SyntheticEventCtor = SyntheticEvent;
  18. let reactEventType: string = domEventName;
  19. switch (domEventName) {
  20. // 按照原生事件名称来获取对应的合成事件构造函数
  21. }
  22. // 是否是捕获阶段
  23. const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  24. if (
  25. enableCreateEventHandleAPI &&
  26. eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  27. ) {
  28. // ...和下文基本一致
  29. } else {
  30. // scroll 事件不冒泡
  31. const accumulateTargetOnly =
  32. !inCapturePhase && domEventName === 'scroll';
  33. // 核心,获取当前阶段的所有事件
  34. const listeners = accumulateSinglePhaseListeners(
  35. targetInst,
  36. reactName,
  37. nativeEvent.type,
  38. inCapturePhase,
  39. accumulateTargetOnly,
  40. );
  41. if (listeners.length > 0) {
  42. // 生成合成事件的 Event 对象
  43. const event = new SyntheticEventCtor(
  44. reactName,
  45. reactEventType,
  46. null,
  47. nativeEvent,
  48. nativeEventTarget,
  49. );
  50. // 入队
  51. dispatchQueue.push({event, listeners});
  52. }
  53. }
  54. }

accumulateSinglePhaseListeners

  1. // 简化版本
  2. export function accumulateSinglePhaseListeners(
  3. targetFiber: Fiber | null,
  4. reactName: string | null,
  5. nativeEventType: string,
  6. inCapturePhase: boolean,
  7. accumulateTargetOnly: boolean,
  8. ): Array<DispatchListener> {
  9. // 捕获阶段合成事件名称
  10. const captureName = reactName !== null ? reactName + 'Capture' : null;
  11. // 最终合成事件名称(这两句是不是有点啰嗦)
  12. const reactEventName = inCapturePhase ? captureName : reactName;
  13. const listeners: Array<DispatchListener> = [];
  14. let instance = targetFiber;
  15. let lastHostComponent = null;
  16. while (instance !== null) {
  17. const {stateNode, tag} = instance;
  18. // 如果是有效节点则获取其事件
  19. if (tag === HostComponent && stateNode !== null) {
  20. lastHostComponent = stateNode;
  21. if (reactEventName !== null) {
  22. // 获取存储在 Fiber 节点上 Props 里的对应事件(如果存在)
  23. const listener = getListener(instance, reactEventName);
  24. if (listener != null) {
  25. // 入队
  26. listeners.push(
  27. // 简单返回一个 {instance, listener, lastHostComponent} 对象
  28. createDispatchListener(instance, listener, lastHostComponent),
  29. );
  30. }
  31. }
  32. }
  33. // scroll 不会冒泡,获取一次就结束了
  34. if (accumulateTargetOnly) {
  35. break;
  36. }
  37. // 其父级 Fiber 节点,向上递归
  38. instance = instance.return;
  39. }
  40. // 返回监听器集合
  41. return listeners;
  42. }

processDispatchQueue(执行事件)

在分析源码之前,我们先整理一下 dispatchQueue 的数据类型,一般来说他的长度只会为 1。

  1. interface dispatchQueue {
  2. event: SyntheticEvent
  3. listeners: {
  4. instance: Fiber,
  5. listener: Function,
  6. currentTarget: Fiber['stateNode']
  7. }[]
  8. }[]

上源码

  1. export function processDispatchQueue(
  2. dispatchQueue: DispatchQueue,
  3. eventSystemFlags: EventSystemFlags,
  4. ): void {
  5. // 通过 eventSystemFlags 判断当前事件阶段
  6. const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  7. // 遍历合成事件
  8. for (let i = 0; i < dispatchQueue.length; i++) {
  9. // 取出其合成 Event 对象及事件集合
  10. const {event, listeners} = dispatchQueue[i];
  11. // 这个函数就负责事件的调用
  12. // 如果是捕获阶段的事件则倒序调用,反之为正序调用,调用时会传入合成 Event 对象
  13. processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  14. }
  15. // 抛出中间产生的错误
  16. rethrowCaughtError();
  17. }

总结

React 合成事件系统其实并不算特别复杂(相较于 Concurrent mode 的相关代码),其核心思想就是用事件代理统一接收事件的触发,然后由 React 自身来调度真实事件的调用,而 React 如何知道应该从哪开始收集事件的核心其实是存储在真实 DOM 上的 Fiber 节点,从真实穿梭到虚拟。

其他

相较于 16.x 版本的区别

React v17.0 RC 发版声明

提三个个人感知比较强烈的变动

1. 监听器挂载的节点由 Document 节点改为 RootNode(根 DOM 节点)

相关 PR:Modern Event System: add plugin handling and forked paths #18195

其主要作用是收束合成事件系统的影响范围,假设我一个 Web 应用中有一个 React 应用的同时还有其他应用也用到了 Document 的事件监听,这个时候很大概率会互相影响导致一方的错误,但当我们将合成事件系统收束到当前 React 应用的根 DOM 节点的时候这种副作用就会减少很多了。

2. 当挂载 root 时,添加所有已知的事件监听器,而不是在 completeWork 阶段按需添加监听器

相关 PR:Attach Listeners Eagerly to Roots and Portal Containers #19659

主要作用是修复了 createPortal 导致事件冒泡的问题,虽然问题不大,但对应源码的变动是真多。

3. 移除事件池

相关 PR:

原先是为了提高性能而存在的设计,但导致事件中异步更新的一些心智负担,所以就在没有太大性能影响的现在移除了。

Vue 有自己的事件机制吗,跟 React 有什么差异?

Vue 其实在事件上也做了很多自己的内容,主要是为了方便开发者的开发,比如各种修饰符或者是各种指令,其核心运作的方式还是依靠其模板编译来实现的,是一个更倾向于编译时的特性,绑定事件也是在其 patch 阶段一个个将事件挂载在对应 DOM 上,而非使用事件代理统一分发。

参考

  1. UI Events | W3C Working Draft, 04 August 2016
  2. Event - Web API 接口参考 | MDN
  3. React v17.0 RC 发版声明
  4. Modern Event System: add plugin handling and forked paths #18195
  5. Attach Listeners Eagerly to Roots and Portal Containers #19659
  6. Remove event pooling in the modern system #18216
  7. Remove event pooling in the modern system #18969
  8. 深入React合成事件机制原理
  9. 谈谈React事件机制和未来(react-events)