React 事件机制源码学习笔记

从一个简单需求开始。

需求描述node

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

代码实现数组

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 依旧会关闭。缓存

能够作以下修改bash

一、经过 e.target 判断。app

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>
    );
  }
}
复制代码

二、仅使用原生事件dom

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,
});
复制代码

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

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

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

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

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

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

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

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

五、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 上。

相关文章
相关标签/搜索