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

相關文章
相關標籤/搜索