React 事件机制源码学习笔记

1,255 阅读9分钟

从一个简单需求开始。

需求描述

点击按钮弹出一个对话框,再次点按钮关闭对话框。点击对话框外的空白区域也可以关闭对话框。

代码实现

class Demo extends PureComponent {
  state = {
    visible: false,
  };
  componentDidMount() {
    document.body.addEventListener('click', () => {
      this.setState({
        visible: false,
      });
    });
  }
  componentWillUnmount() {
    document.body.removeEventListener('click');
  }
  handleBtnClick = (e) => {
    e.preventDefault();
    const { visible } = this.state;
    this.setState({
      visible: !visible,
    });
  }
  handleDialogClick = (e) => {
    e.preventDefault();
  }
  render() {
    const { visible } = this.state;
    return (
      <div>
        <div
          onClick={this.handleDialogClick}
          style={{
            display: visible ? 'block' : 'none',
            position: 'fixed',
            top: 100,
            left: '50%',
            marginLeft: -190,
            width: 380,
            height: 300,
            background: '#fff',
            zIndex: 999
          }}
        >
          喵喵~~~
        </div>
        <Button onClick={this.handleBtnClick}>{visible ? 'close' : 'open'}</Button>
      </div>
    );
  }
}

很完美有没有?简直毫无破绽[捂脸]

但实际上的效果并不是我们想要的,点击 Dialog 依旧会关闭。

可以做如下修改

1、通过 e.target 判断。

class Demo extends PureComponent {
  state = {
    visible: false,
  };
  componentDidMount() {
    document.body.addEventListener('click', (e) => {
      if (e.target && (e.target.matches('.dialog') || e.target.matches('.btn'))) {
        return;
      }
      this.setState({
        visible: false,
      });
    });
  }
  componentWillUnmount() {
    document.body.removeEventListener('click');
  }
  handleBtnClick = (e) => {
    const { visible } = this.state;
    this.setState({
      visible: !visible,
    });
  }
  render() {
    const { visible } = this.state;
    return (
      <div>
        <div
          className="dialog"
          style={{
            display: visible ? 'block' : 'none',
            position: 'fixed',
            top: 100,
            left: '50%',
            marginLeft: -190,
            width: 380,
            height: 300,
            background: '#fff',
            zIndex: 999
          }}
        >
          喵喵~~~
        </div>
        <Button onClick={this.handleBtnClick} className="btn">{visible ? 'close' : 'open'}</Button>
      </div>
    );
  }
}

2、仅使用原生事件

class Demo extends PureComponent {
  state = {
    visible: false,
  };
  componentDidMount() {
    document.body.addEventListener('click', (e) => {
      if (e.target && e.target.matches('.dialog')) {
        return;
      }
      this.setState({
        visible: false,
      });
    });
    document.querySelector('.btn').addEventListener('click', (e) => {
      e.preventDefault();
      e.cancelBubble = true;
      const { visible } = this.state;
      this.setState({
        visible: !visible,
      });
    });
  }
  componentWillUnmount() {
    document.body.removeEventListener('click');
    document.querySelector('.dialog').removeEventListener('click');
  }
  render() {
    const { visible } = this.state;
    return (
      <div>
        <div
          className="dialog"
          style={{
            display: visible ? 'block' : 'none',
            position: 'fixed',
            top: 100,
            left: '50%',
            marginLeft: -190,
            width: 380,
            height: 300,
            background: '#fff',
            zIndex: 999
          }}
        >
          喵喵~~~
        </div>
        <Button className="btn">{visible ? 'close' : 'open'}</Button>
      </div>
    );
  }
}

看到这里,是不是有发现点什么了?

React 事件机制

React 基于 Virtual Dom 实现了一个事件合成的机制,我们所注册的事件,会合成一个 SyntheticEvent 对象,如果想访问原生的事件对象,可以访问 nativeEvent 属性。React 事件机制,消除了浏览器的兼容性问题,并且保持与原生事件一致的表现。

源码分析

入口

packages/react-dom/src/events/ReactBrowserEventEmitter.js

/**
 * 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.
 *
 * 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 |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 *    React Core     .  General Purpose Event Plugin System
 */

按照流程图的顺序浏览下事件机制的实现

事件注册与存储

一切故事从这里开始...

packages/react-dom/src/client/ReactDOMComponent.js

ReactDOMComponent 会遍历 ReactNode 的 props 对象,设置待渲染的真实 DOM 对象的一系列的属性,也包括事件注册。

// function diffProperties
if (registrationNameModules.hasOwnProperty(propKey)) {
  if (nextProp != null) {
    // 尚未委托事件时异常
    if (__DEV__ && typeof nextProp !== 'function') {
      warnForInvalidEventListener(propKey, nextProp);
    }
    // 处理事件类型的 props
    ensureListeningTo(rootContainerElement, propKey);
  }
  // ...
}

事件委托,所有的事件最终都会被委托到 document 或者 fragment上去

function ensureListeningTo(
  rootContainerElement: Element | Node,
  registrationName: string, // registrationName:传过来的 onClick
): void {
  const isDocumentOrFragment = 
    rootContainerElement.nodeType === DOCUMENT_NODE 
    || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  // 取出 element 所在的 document
  const doc = isDocumentOrFragment
    ? rootContainerElement
    : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}

继续看 listenTo 的代码

export function listenTo(
  registrationName: string,
  mountAt: Document | Element | Node,
): void {
  const listeningSet = getListeningSetForElement(mountAt);
  // registrationNameDependencies 存储了 React 事件名与浏览器原生事件名对应的一个 Map
  const dependencies = registrationNameDependencies[registrationName];

  for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    // 调用该方法进行注册
    listenToTopLevel(dependency, mountAt, listeningSet);
  }
}

listenToTopLevel 方法

export function listenToTopLevel(
  topLevelType: DOMTopLevelEventType,
  mountAt: Document | Element | Node,
  listeningSet: Set<DOMTopLevelEventType | string>,
): void {
    if (!listeningSet.has(topLevelType)) {
      switch (topLevelType) {
        case TOP_SCROLL:
          // trapCapturedEvent 捕获事件
          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(topLevelType))) {
            trapCapturedEvent(topLevelType, mountAt);
          }
          break;
        case TOP_INVALID:
        case TOP_SUBMIT:
        case TOP_RESET:
          // 在目标 DOM 元素上监听,会冒泡的直接跳过
          break;
        default:
          // 默认情况,在顶层监听所有非媒体事件,媒体事件不会冒泡,因此添加侦听器不会做任何事情
          const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
          if (!isMediaEvent) {
            // trapBubbledEvent 冒泡
            trapBubbledEvent(topLevelType, mountAt); 
          }
          break;
      }
      listeningSet.add(topLevelType);
    }
}

捕获事件 && 事件冒泡

// 捕获事件
export function trapCapturedEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element | Node,
): void {
  trapEventForPluginEventSystem(element, topLevelType, true);
}

// 事件冒泡
export function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element | Node,
): void {
  trapEventForPluginEventSystem(element, topLevelType, false);
}

function trapEventForPluginEventSystem(
  element: Document | Element | Node,
  topLevelType: DOMTopLevelEventType,
  capture: boolean, // capture true 捕获, false 冒泡
): void {
  // ...
  if (capture) {
    // 捕获事件
    addEventCaptureListener(element, rawEventName, listener);
  } else {
    // 冒泡
    addEventBubbleListener(element, rawEventName, listener);
  }
}

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

事件注册上了,那然后呢?

事件合成

继续看 EventPluginHub,它负责管理和注册各种插件。React 事件系统使用了插件机制来管理不同行为的事件,这些插件会处理对应类型的事件,并生成合成事件对象。

在 ReactDOM 启动时就会向 EventPluginHub 注册以下插件

// packages/react-dom/src/client/ReactDOMClientInjection.js
EventPluginHubInjection.injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});

1、packages/react-dom/src/events/ChangeEventPlugin.js

change事件是React的一个自定义事件,旨在规范化表单元素的变动事件。 它支持这些表单元素: input, textarea, select

2、packages/react-dom/src/events/EnterLeaveEventPlugin.js

mouseEnter mouseLeave 和 pointerEnter pointerLeave 这两类比较特殊的事件

3、packages/react-dom/src/events/SelectEventPlugin.js

和 change 事件一样,React 为表单元素规范化了 select (选择范围变动)事件,适用于 input、textarea、contentEditable 元素.

4、packages/react-dom/src/events/SimpleEventPlugin.js

简单事件, 处理一些比较通用的事件类型

5、packages/react-dom/src/events/BeforeInputEventPlugin.js

beforeinput 事件

分析下 SimpleEventPlugin

/**
 * Turns
 * ['abort', ...]
 * into
 * eventTypes = {
 *   'abort': {
 *     phasedRegistrationNames: {
 *       bubbled: 'onAbort',
 *       captured: 'onAbortCapture',
 *     },
 *     dependencies: [TOP_ABORT],
 *   },
 *   ...
 * };
 * topLevelEventsToDispatchConfig = new Map([
 *   [TOP_ABORT, { sameConfig }],
 * ]);
 */
// 生成一个合成事件,每个 plugin 都有这个函数
extractEvents: function(
  topLevelType: TopLevelType,
  eventSystemFlags: EventSystemFlags,
  targetInst: null | Fiber,
  nativeEvent: MouseEvent,
  nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
  const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
  if (!dispatchConfig) {
    return null;
  }
  // ...
  // 从对象池中取出这个 event 的一个实例
  const event = EventConstructor.getPooled(
    dispatchConfig,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  );
  accumulateTwoPhaseDispatches(event);
  return event;
}

EventPropagators

// packages/legacy-events/EventPropagators.js

// 这个函数的作用是给合成事件加上 listener,最终所有同类型的 listener 都会放到 _dispatchListeners 里
function accumulateDirectionalDispatches(inst, phase, event) {
  if (__DEV__) {
    warningWithoutStack(inst, 'Dispatching inst must not be null');
  }
  // 根据事件阶段的不同取出响应的事件
  const listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    // 这里将所有的 listener 都存入 _dispatchListeners 中
    // _dispatchListeners = [onClick, outClick]
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

// 找到不同阶段(捕获/冒泡)元素绑定的回调函数 listener
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
  const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
  return getListener(inst, registrationName);
}

// packages/legacy-events/EventPluginHub.js
/**
 * @param {object} inst The instance, which is the source of events.
 * @param {string} registrationName Name of listener (e.g. `onClick`).
 * @return {?function} The stored callback.
 */
export function getListener(inst: Fiber, registrationName: string) {
  let listener;

  // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
  // live here; needs to be moved to a better place soon
  const stateNode = inst.stateNode;
  if (!stateNode) {
    // Work in progress (ex: onload events in incremental mode).
    return null;
  }
  const props = getFiberCurrentPropsFromNode(stateNode);
  if (!props) {
    // Work in progress.
    return null;
  }
  listener = props[registrationName];
  if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
    return null;
  }
  invariant();
  return listener;
}

总结:合成事件收集了一波同类型例如 click 的回调函数存在了 event._dispatchListeners 里

事件分发与执行

注册到 document 上的事件,对应的回调函数都会触发 dispatchEvent 方法,它是事件分发的入口方法。

export function dispatchEvent(
  topLevelType: DOMTopLevelEventType, // 带 top 的事件名,如 topClick。
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent, // 用户触发 click 等事件时,浏览器传递的原生事件
): void {
  if (!_enabled) {
    return;
  }
  if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) {
    // 已经有一个事件队列,这是另外一个事件
    // 事件需要按顺序分发.
    queueDiscreteEvent(
      null,
      topLevelType,
      eventSystemFlags,
      nativeEvent,
    );
    return;
  }

  const blockedOn = attemptToDispatchEvent(
    topLevelType,
    eventSystemFlags,
    nativeEvent,
  );

  if (blockedOn === null) {
    // We successfully dispatched this event.
    clearIfContinuousEvent(topLevelType, nativeEvent);
    return;
  }

  if (isReplayableDiscreteEvent(topLevelType)) {
    // This this to be replayed later once the target is available.
    queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, nativeEvent);
    return;
  }

  if (
    queueIfContinuousEvent(
      blockedOn,
      topLevelType,
      eventSystemFlags,
      nativeEvent,
    )
  ) {
    return;
  }

  // 因为排队是累积性的,所以只有在不排队时才需要清除
  clearIfContinuousEvent(topLevelType, nativeEvent);

  // in case the event system needs to trace it.
  if (enableFlareAPI) {
    if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
      dispatchEventForPluginEventSystem(
        topLevelType,
        eventSystemFlags,
        nativeEvent,
        null,
      );
    }
    if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
      // React Flare event system
      dispatchEventForResponderEventSystem(
        (topLevelType: any),
        null,
        nativeEvent,
        getEventTarget(nativeEvent),
        eventSystemFlags,
      );
    }
  } else {
    dispatchEventForPluginEventSystem(
      topLevelType,
      eventSystemFlags,
      nativeEvent,
      null,
    );
  }
}


function dispatchEventForPluginEventSystem(
  topLevelType: DOMTopLevelEventType,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
): void {
  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
    eventSystemFlags,
  );

  try {
    // 允许在同一周期内处理事件队列
    // 阻止默认行为 preventDefault
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

function dispatchEventForPluginEventSystem(
  topLevelType: DOMTopLevelEventType,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
): void {
  // bookKeeping 用来保存过程中会使用到的变量的对象。初始化使用了 react 在源码中用到的对象池的方法来避免多余的垃圾回收,
  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
    eventSystemFlags,
  );

  try {
    // 允许在同一周期内处理事件队列
    // 阻止默认行为 preventDefault
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

事件分发的核心,使用批处理的方式进行事件分发,handleTopLevel 是事件分发的真正执行者。它主要做两件事情,一是利用浏览器回传的原生事件构造出 React 合成事件,二是采用队列的方式处理 events。

function handleTopLevel(bookKeeping: BookKeepingInstance) {
  let targetInst = bookKeeping.targetInst;
  //遍历层次结构,以防存在任何嵌套的组件。
  //重要的是我们在调用任何祖先之前先建立父数组
  //事件处理程序,因为事件处理程序可以修改 DOM,从而导致与 ReactMount 的节点缓存不一致。
  let ancestor = targetInst;
  // 事件回调函数执行后可能导致 Virtual DOM 结构的变化。
  // 执行前,先存储事件触发时的 DOM 结构
  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);
  // 依次遍历数组,并执行回调函数,这个顺序就是冒泡的顺序
  // 不能通过 stopPropagation 来阻止冒泡。
  for (let i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    // 事件触发的 DOM
    const eventTarget = getEventTarget(bookKeeping.nativeEvent);
    const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
    // 原生事件 event
    const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);
    runExtractedPluginEventsInBatch(
      topLevelType,
      targetInst,
      nativeEvent,
      eventTarget,
      bookKeeping.eventSystemFlags,
    );
  }
}

React 实现了一套冒泡机制,从触发事件的对象开始,向父元素回溯,依次调用它们注册的事件回调函数。

总结

我们在 React 中定义的事件处理器会接收到一个合成事件对象的示例(使用 nativeEvent 可以访问原生事件对象),React 消除了它在不同浏览器中的兼容性问题,与原生的浏览器事件一样拥有同样的接口,同样支持冒泡机制,可以试用 stopPropagation() 和 preventDefault() 终端它。除一些媒体事件(例如 onplay onpause),React 并不会把事件直接绑定到真实节点上,而是把事件代理到到 document 上。