深入理解React事件机制

11/8/2023 React

React 的事件机制是 React 应用中的一个关键部分,它负责处理用户交互和事件响应。本文将深入讨论 React 事件机制的工作原理,包括事件注册、事件合成和事件分发。

# 事件注册

# Root 节点的统一监听

React 在 createRoot 阶段的时候,就会对 root 节点绑定浏览器所有的事件如 onclick, onchange,message 等,并根据事件冒泡和捕获的机制来区分绑定的方法,因为对于后续的事件触发,React 大部分的事件是通过目标 DOM 的事件冒泡或捕获的方式来集中到 root 节点去处理,而不是单独对于目标 DOM 去绑定(当然有一些事件会做特殊处理,例如 input 的 invalid 就直接绑定给 DOM 对象上,因为没有冒泡或者捕获)。

ps: 如果绑定 input onchange 事件,React 也会帮助你绑定 onInput 事件来更好完成这个事件。

ps2: 由于事件绑定在 IE 和其他浏览器的不同,React 也会帮助兼容各个浏览器。

React 会根据事件类型去区分不同的事件优先级用户主动操作的事件都是 ContinuousEventPriority,而通过 Scheduler 去调度的 render 则根据更新优先级去判断事件优先级;

优先级分为:

  1. DiscreteEventPriority
  2. ContinuousEventPriority
  3. IdleEventPriority
  4. DefaultEventPriority

根据优先级的不同,设置给 currentUpdatePriority(此处用于后续的调度优先级),但最后还是交给 dispatchEvent 方法去分发事件。

# 虚拟 DOM 的单独注册

以前的 React 版本中,在渲染阶段,会通过对比 props 中的属性,把事件回调储存在 WeakMap 里面,当在事件分发阶段时就会取出来执行。

而在 React 18 版本中,最后在 commit 阶段,仅对一些特殊的事件,例如 invalid 做处理,而其他常见的事件不处理(onclick 会添加一个 noop 函数,这是处理 safari 的 BUG),把整个 props 存储到 fiber 中。等事件分发阶段的时,再从整个 props 中挑取对应的回调来执行。

export function commitUpdate(
  domElement: Instance,
  updatePayload: any,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object
): void {
  // Diff and update the properties.
  updateProperties(domElement, type, oldProps, newProps);

  // Update the props handle so that we know which props are the ones with
  // with current event handlers.
  updateFiberProps(domElement, newProps);
}

# 事件合成

React 在触发事件执行 dispatchEvent 事件的时候,根据事件的类型来合成事件,不同的事件类型对应不同的事件插件,每个插件都会添加自己特殊的属性和逻辑。

  1. SimpleEventPlugin (默认事件插件,所有事件都会使用)
  2. EnterLeaveEventPlugin (enter/leave 相关的事件,例如 pointerEnter, pointerLeave)
  3. FormActionEventPlugin (表单相关事件)
  4. ChangeEventPlugin (change 相关的事件,例如 input onChange 会使用原生各种事件合成实现)
  5. SelectEventPlugin (select 事件)
  6. BeforeInputEventPlugin(Input 相关的事件,也是通过原生各种事件来帮忙实现)

在以前的 React 版本中,React 还有事件池的概念,就是所有的 React 事件会从一个池子里获取对象,使用完后归还来减少内存。

如果为了继续使用,不让回收,可以使用下面的方法:

  1. event.persist(), 告诉 React 不要回收
  2. 缓存并使用event.nativeEvent, 这个是原生的事件对象,不会回收

但在 React 18 中,取消了这个概念,直接 new SyntheticBaseEvent 这个事件基类去合成。

function createSyntheticEvent(Interface: EventInterfaceType) {
  /**
   * Synthetic events are dispatched by event plugins, typically in response to a
   * top-level event delegation handler.
   *
   * These systems should generally use pooling to reduce the frequency of garbage
   * collection. The system should check `isPersistent` to determine whether the
   * event should be released into the pool after being dispatched. Users that
   * need a persisted event should invoke `persist`.
   *
   * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
   * normalizing browser quirks. Subclasses do not necessarily have to implement a
   * DOM interface; custom application-specific events can also subclass this.
   */
  // $FlowFixMe[missing-this-annot]
  function SyntheticBaseEvent(
    reactName: string | null,
    reactEventType: string,
    targetInst: Fiber | null,
    nativeEvent: { [propName: string]: mixed, ... },
    nativeEventTarget: null | EventTarget
  ) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;

    for (const propName in Interface) {
      if (!Interface.hasOwnProperty(propName)) {
        continue;
      }
      const normalize = Interface[propName];
      if (normalize) {
        this[propName] = normalize(nativeEvent);
      } else {
        this[propName] = nativeEvent[propName];
      }
    }

    const defaultPrevented =
      nativeEvent.defaultPrevented != null
        ? nativeEvent.defaultPrevented
        : nativeEvent.returnValue === false;
    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }
    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  // $FlowFixMe[prop-missing] found when upgrading Flow
  assign(SyntheticBaseEvent.prototype, {
    // $FlowFixMe[missing-this-annot]
    preventDefault: function () {
      this.defaultPrevented = true;
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.preventDefault) {
        event.preventDefault();
        // $FlowFixMe[illegal-typeof] - flow is not aware of `unknown` in IE
      } else if (typeof event.returnValue !== 'unknown') {
        event.returnValue = false;
      }
      this.isDefaultPrevented = functionThatReturnsTrue;
    },

    // $FlowFixMe[missing-this-annot]
    stopPropagation: function () {
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.stopPropagation) {
        event.stopPropagation();
        // $FlowFixMe[illegal-typeof] - flow is not aware of `unknown` in IE
      } 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;
    },

    /**
     * We release all dispatched `SyntheticEvent`s after each event loop, adding
     * them back into the pool. This allows a way to hold onto a reference that
     * won't be added back into the pool.
     */
    persist: function () {
      // Modern event system doesn't use pooling.
    },

    /**
     * Checks if this event should be released back into the pool.
     *
     * @return {boolean} True if this should not be released, false otherwise.
     */
    isPersistent: functionThatReturnsTrue,
  });
  return SyntheticBaseEvent;
}

# 事件分发

在执行 dispatchEvent 的时候,会根据 DOM 来获取对应 Fiber 对象,然后根据 Fiber 获取 props,并在 props 中获取对应事件的回调。

在新版本中,React 会执行两次事件,包括事件的捕获和冒泡阶段。更接近原生事件执行顺序,提高了事件处理的准确性。

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean
): void {
  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 会在事件底层用一个数组队列来收集 fiber 树上一条分支上的所有的 onClick 和 onClickCapture 事件,遇到捕获阶段执行的事件,比如 onClickCapture,就会通过 unshift 放在数组的前面,如果遇到冒泡阶段执行的事件,比如 onClick,就会通过 push 放在数组的后面,最后依次执行队列中的事件处理函数,模拟事件流。