React中的合成事件

React中的合成事件

React本身實現了一套高效的事件註冊、存儲、分發和重用邏輯,在DOM事件體系基礎上作了很大改進,減小了內存消耗,簡化了事件邏輯,並最大程度地解決了IE等瀏覽器的不兼容問題。javascript

描述

React的合成事件SyntheticEvent實際上就是React本身在內部實現的一套事件處理機制,它是瀏覽器的原生事件的跨瀏覽器包裝器,除兼容全部瀏覽器外,它還擁有和瀏覽器原生事件相同的接口,包括stopPropagation()preventDefault(),合成事件與瀏覽器的原生事件不一樣,也不會直接映射到原生事件,也就是說一般不要使用addEventListener爲已建立的DOM元素添加監聽器,而應該直接使用React中定義的事件機制,並且在混用的狀況下原生事件若是定義了阻止冒泡可能會阻止合成事件的執行,固然若是確實須要使用原生事件去處理需求,能夠經過事件觸發傳遞的SyntheticEvent對象的nativeEvent屬性得到原生Event對象的引用,React中的事件有如下幾個特色:html

  • React上註冊的事件最終會綁定在document這個DOM上,而不是React組件對應的DOM,經過這種方式減小內存開銷,全部的事件都綁定在document上,其餘節點沒有綁定事件,實際上就是事件委託的。
  • React自身實現了一套事件冒泡機制,使用React實現的Event對象與原生Event對象不一樣,不能相互混用。
  • React經過隊列的形式,從觸發的組件向父組件回溯,而後調用他們JSX中定義的callback
  • React的合成事件SyntheticEvent與瀏覽器的原生事件不一樣,也不會直接映射到原生事件。
  • React經過對象池的形式管理合成事件對象的建立和銷燬,減小了垃圾的生成和新對象內存的分配,提升了性能。

對於每一個SyntheticEvent對象都包含如下屬性:java

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

支持的合成事件一覽,注意如下的事件處理函數在冒泡階段被觸發,如需註冊捕獲階段的事件處理函數,則應爲事件名添加Capture,例如處理捕獲階段的點擊事件請使用onClickCapture,而不是onClicknode

<!-- 剪貼板事件 -->
onCopy onCut onPaste

<!-- 複合事件 -->
onCompositionEnd onCompositionStart onCompositionUpdate

<!-- 鍵盤事件 -->
onKeyDown onKeyPress onKeyUp

<!-- 焦點事件 -->
onFocus onBlur

<!-- 表單事件 -->
onChange onInput onInvalid onReset onSubmit 

<!-- 通用事件 -->
onError onLoad

<!-- 鼠標事件 -->
onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit
onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave
onMouseMove onMouseOut onMouseOver onMouseUp

<!-- 指針事件 -->
onPointerDown onPointerMove onPointerUp onPointerCancel onGotPointerCapture
onLostPointerCapture onPointerEnter onPointerLeave onPointerOver onPointerOut

<!-- 選擇事件 -->
onSelect

<!-- 觸摸事件 -->
onTouchCancel onTouchEnd onTouchMove onTouchStart

<!-- UI 事件 -->
onScroll

<!-- 滾輪事件 -->
onWheel

<!-- 媒體事件 -->
onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted
onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay
onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend
onTimeUpdate onVolumeChange onWaiting

<!-- 圖像事件 -->
onLoad onError

<!-- 動畫事件 -->
onAnimationStart onAnimationEnd onAnimationIteration

<!-- 過渡事件 -->
onTransitionEnd

<!-- 其餘事件 -->
onToggle

<!-- https://zh-hans.reactjs.org/docs/events.html -->

示例

一個簡單的示例,同時綁定在一個DOM上的原生事件與React事件,由於原生事件阻止冒泡而致使React事件沒法執行,同時咱們也能夠看到React傳遞的event並非原生Event對象的實例,而是React自行實現維護的一個event對象。react

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>React</title>
</head>

<body>
    <div id="root"></div>
</body>
<script src="https://unpkg.zhimg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.zhimg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.zhimg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">

    class ReactEvent extends React.PureComponent {
        componentDidMount(){ 
            document.getElementById("btn-reactandnative").addEventListener("click", (e) => {
                console.log("原生事件執行", "handleNativeAndReact");
                console.log("event instanceof Event:", e instanceof Event);
                e.stopPropagation(); // 阻止冒泡即會影響了React的事件執行
            });
        }


        handleNativeAndReact = (e) => {
            console.log("React事件執行", "handleNativeAndReact");
            console.log("event instanceof Event:", e instanceof Event);
        }


        handleClick = (e) => {
            console.log("React事件執行", "handleClick");
            console.log("event instanceof Event:", e instanceof Event);
        }

      render() {
        return (
            <div className="pageIndex">
                <button id="btn-confirm" onClick={this.handleClick}>React 事件</button>
                <button id="btn-reactandnative" onClick={this.handleNativeAndReact}>原生 + React 事件</button>
            </div>
        )
      }
    }



    var vm = ReactDOM.render(
        <>
            <ReactEvent />
        </>,
        document.getElementById("root")
    );
</script>

</html>

React事件系統

簡單來講,在掛載的時候,經過listenerBank把事件存起來了,觸發的時候document進行dispatchEvent,找到觸發事件的最深的一個節點,向上遍歷拿到全部的callback放在eventQueue,根據事件類型構建event對象,遍歷執行eventQueue,不簡單點說,咱們能夠查看一下React對於事件處理的源碼實現,commit id4ab6305TAGReact16.10.2,在React17再也不往document上掛事件委託,而是掛到DOM容器上,目錄結構都有了很大更改,咱們仍是依照React16,首先來看一下事件的處理流程。git

/**
 * 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.
 */
 
/**
 * React和事件系統概述:
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 */

packages\react-dom\src\events\ReactBrowserEventEmitter.js中就描述了上邊的流程,而且還有相應的英文註釋,使用google翻譯一下,這個太概述了,因此仍是須要詳細描述一下,在事件處理以前,咱們編寫的JSX須要通過babel的編譯,建立虛擬DOM,並處理組件props,拿到事件類型和回調fn等,以後即是事件註冊、存儲、合成、分發、執行階段。github

  • Top-level delegation用於捕獲最原始的瀏覽器事件,它主要由ReactEventListener負責,ReactEventListener被注入後能夠支持插件化的事件源,這一過程發生在主線程。
  • React對事件進行規範化和重複數據刪除,以解決瀏覽器的問題,這能夠在工做線程中完成。
  • 將這些本地事件(具備關聯的頂級類型用來捕獲它)轉發到EventPluginHub,後者將詢問插件是否要提取任何合成事件。
  • 而後EventPluginHub將經過爲每一個事件添加dispatches(引用該事件的偵聽器和ID的序列)來對其進行註釋來進行處理。
  • 再接着,EventPluginHub會調度分派事件。

事件註冊

首先會調用setInitialDOMProperties()判斷是否在registrationNameModules列表中,在的話便註冊事件,列表包含了能夠註冊的事件。segmentfault

// packages\react-dom\src\client\ReactDOMComponent.js line 308
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      if (__DEV__) {
        if (nextProp) {
          // Freeze the next style object so that we can assume it won't be
          // mutated. We have already warned for this in the past.
          Object.freeze(nextProp);
        }
      }
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      setValueForStyles(domElement, nextProp);
    }else if(/* ... */){
        // ...
    } else if (registrationNameModules.hasOwnProperty(propKey)) { // 對事件名進行合法性檢驗,只有合法的事件名纔會被識別並進行事件綁定
      if (nextProp != null) {
        if (__DEV__ && typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }
        ensureListeningTo(rootContainerElement, propKey); // 開始註冊事件
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

若是事件名合法並且是一個函數的時候,就會調用ensureListeningTo()方法註冊事件。ensureListeningTo會判斷rootContainerElement是否爲document或是Fragment,若是是則直接傳遞給listenTo,若是不是則經過ownerDocument來獲取其根節點,對於ownerDocument屬性,定義是這樣的,ownerDocument可返回某元素的根元素,在HTMLHTML文檔自己是元素的根元素,因此能夠說明其實大部分的事件都是註冊在document上面的,以後即是調用listenTo方法實際註冊。瀏覽器

// packages\react-dom\src\client\ReactDOMComponent.js line 272
function ensureListeningTo(
  rootContainerElement: Element | Node,
  registrationName: string,
): void {
  const isDocumentOrFragment =
    rootContainerElement.nodeType === DOCUMENT_NODE ||
    rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  const doc = isDocumentOrFragment
    ? rootContainerElement
    : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}

listenTo()方法中比較重要的就是registrationNameDependencies的概念,對於不一樣的事件,React會同時綁定多個事件來達到統一的效果。此外listenTo()方法還默認將事件經過trapBubbledEvent綁定,將onBluronFocusonScroll等事件經過trapCapturedEvent綁定,由於這些事件沒有冒泡行爲,invalidsubmitreset事件以及媒體等事件綁定到當前DOM上。緩存

// packages\react-dom\src\events\ReactBrowserEventEmitter.js line 128
export function listenTo(
  registrationName: string, // 事件的名稱,即爲上面的propKey(如onClick)
  mountAt: Document | Element | Node, // 事件註冊的目標容器
): void {
  // 獲取目標容器已經掛載的事件列表對象,若是沒有則初始化爲空對象
  const listeningSet = getListeningSetForElement(mountAt);
  // 獲取對應事件的依賴事件,好比onChange會依賴TOP_INPUT、TOP_FOCUS等一系列事件
  const dependencies = registrationNameDependencies[registrationName];
    
   // 遍歷全部的依賴,並挨個進行綁定
  for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    listenToTopLevel(dependency, mountAt, listeningSet);
  }
}

export function listenToTopLevel(
  topLevelType: DOMTopLevelEventType,
  mountAt: Document | Element | Node,
  listeningSet: Set<DOMTopLevelEventType | string>,
): void {
  if (!listeningSet.has(topLevelType)) {
    // 針對不一樣的事件來判斷使用事件捕獲仍是事件冒泡
    switch (topLevelType) {
      case TOP_SCROLL:
        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:
        // getRawEventName會返回真實的事件名稱,好比onChange => onchange
        if (isEventSupported(getRawEventName(topLevelType))) {
          trapCapturedEvent(topLevelType, mountAt);
        }
        break;
      case TOP_INVALID:
      case TOP_SUBMIT:
      case TOP_RESET:
        // We listen to them on the target DOM elements.
        // Some of them bubble so we don't want them to fire twice.
        break;
      default:
        // 默認將除了媒體事件以外的全部事件都註冊冒泡事件
        // 由於媒體事件不會冒泡,因此註冊冒泡事件毫無心義
        const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
        if (!isMediaEvent) {
          trapBubbledEvent(topLevelType, mountAt);
        }
        break;
    }
    // 表示目標容器已經註冊了該事件
    listeningSet.add(topLevelType);
  }
}

以後就是熟知的對事件的綁定,以事件冒泡trapBubbledEvent()爲例來描述處理流程,能夠看到其調用了trapEventForPluginEventSystem方法。

// packages\react-dom\src\events\ReactDOMEventListener.js line 203
export function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element | Node,
): void {
  trapEventForPluginEventSystem(element, topLevelType, false);
}

能夠看到React將事件分紅了三類,優先級由低到高:

  • DiscreteEvent離散事件,例如blurfocusclicksubmittouchStart,這些事件都是離散觸發的。
  • UserBlockingEvent用戶阻塞事件,例如touchMovemouseMovescrolldragdragOver等等,這些事件會阻塞用戶的交互。
  • ContinuousEvent連續事件,例如loaderrorloadStartabortanimationEnd,這個優先級最高,也就是說它們應該是當即同步執行的,這就是Continuous的意義,是持續地執行,不能被打斷。

此外React將事件系統用到了Fiber架構裏,Fiber中將任務分紅了5大類,對應不一樣的優先級,那麼三大類的事件系統和五大類的Fiber任務系統的對應關係以下。

  • Immediate: 此類任務會同步執行,或者說立刻執行且不能中斷,ContinuousEvent便屬於此類。
  • UserBlocking: 此類任務通常是用戶交互的結果,須要及時獲得反饋,DiscreteEventUserBlockingEvent都屬於此類。
  • Normal: 此類任務是應對那些不須要當即感覺到反饋的任務,好比網絡請求。
  • Low: 此類任務能夠延後處理,但最終應該獲得執行,例如分析通知。
  • Idle: 此類任務的定義爲沒有必要作的任務。

回到trapEventForPluginEventSystem,實際上在這三類事件,他們最終都會有統一的觸發函數dispatchEvent,只不過在dispatch以前會須要進行一些特殊的處理。

// packages\react-dom\src\events\ReactDOMEventListener.js line 256
function trapEventForPluginEventSystem(
  element: Document | Element | Node,
  topLevelType: DOMTopLevelEventType,
  capture: boolean,
): void {
  let listener;
  switch (getEventPriority(topLevelType)) {
    case DiscreteEvent:
      listener = dispatchDiscreteEvent.bind(
        null,
        topLevelType,
        PLUGIN_EVENT_SYSTEM,
      );
      break;
    case UserBlockingEvent:
      listener = dispatchUserBlockingUpdate.bind(
        null,
        topLevelType,
        PLUGIN_EVENT_SYSTEM,
      );
      break;
    case ContinuousEvent:
    default:
      // 統一的分發函數 dispatchEvent
      listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
      break;
  }

  const rawEventName = getRawEventName(topLevelType);
  if (capture) {
    // 註冊捕獲事件
    addEventCaptureListener(element, rawEventName, listener);
  } else {
    // 註冊冒泡事件
    addEventBubbleListener(element, rawEventName, listener);
  }
}

到達最終的事件註冊,實際上就是在document上註冊了各類事件。

// packages\react-dom\src\events\EventListener.js line 10
export function addEventBubbleListener(
  element: Document | Element | Node,
  eventType: string,
  listener: Function,
): void {
  element.addEventListener(eventType, listener, false);
}

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

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

事件存儲

讓咱們回到上邊的listenToTopLevel方法中的listeningSet.add(topLevelType),便是將事件添加到註冊到事件列表對象中,即將DOM節點和對應的事件保存到Weak Map對象中,具體來講就是DOM節點做爲鍵名,事件對象的Set做爲鍵值,這裏的數據集合有本身的名字叫作EventPluginHub,固然在這裏最理想的狀況會是使用WeakMap進行存儲,不支持則使用Map對象,使用WeakMap主要是考慮到WeakMaps保持了對鍵名所引用的對象的弱引用,不用擔憂內存泄漏問題,WeakMaps應用的典型場合就是DOM節點做爲鍵名。

// packages\react-dom\src\events\ReactBrowserEventEmitter.js line 88
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const elementListeningSets:
  | WeakMap
  | Map<
      Document | Element | Node,
      Set<DOMTopLevelEventType | string>,
    > = new PossiblyWeakMap();

export function getListeningSetForElement(
  element: Document | Element | Node,
): Set<DOMTopLevelEventType | string> {
  let listeningSet = elementListeningSets.get(element);
  if (listeningSet === undefined) {
    listeningSet = new Set();
    elementListeningSets.set(element, listeningSet);
  }
  return listeningSet;
}

事件合成

首先來看看handleTopLevel的邏輯,handleTopLevel主要是緩存祖先元素,避免事件觸發後找不到祖先元素報錯,接下來就進入runExtractedPluginEventsInBatch方法。

// packages\react-dom\src\events\ReactDOMEventListener.js line 151
function handleTopLevel(bookKeeping: BookKeepingInstance) {
  let targetInst = bookKeeping.targetInst;

  // Loop through the hierarchy, in case there's any nested components.
  // It's important that we build the array of ancestors before calling any
  // event handlers, because event handlers can modify the DOM, leading to
  // inconsistencies with ReactMount's node cache. See #1105.
  let ancestor = targetInst;
  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);

  for (let i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    const eventTarget = getEventTarget(bookKeeping.nativeEvent);
    const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
    const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);

    runExtractedPluginEventsInBatch(
      topLevelType,
      targetInst,
      nativeEvent,
      eventTarget,
      bookKeeping.eventSystemFlags,
    );
  }
}

runExtractedPluginEventsInBatchextractPluginEvents用於經過不一樣的插件合成事件events,而runEventsInBatch則是完成事件的觸發。

// packages\legacy-events\EventPluginHub.js line 160
export function runExtractedPluginEventsInBatch(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
  eventSystemFlags: EventSystemFlags,
) {
  const events = extractPluginEvents(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
  );
  runEventsInBatch(events);
}

extractPluginEvents中遍歷全部插件的extractEvents方法合成事件,若是這個插件適合於這個events則返回它,不然返回null。默認的有5種插件SimpleEventPluginEnterLeaveEventPluginChangeEventPluginSelectEventPluginBeforeInputEventPlugin

// packages\legacy-events\EventPluginHub.js line 133
function extractPluginEvents(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
  eventSystemFlags: EventSystemFlags,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
  let events = null;
  for (let i = 0; i < plugins.length; i++) {
    // Not every plugin in the ordering may be loaded at runtime.
    const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
    if (possiblePlugin) {
      const extractedEvents = possiblePlugin.extractEvents(
        topLevelType,
        targetInst,
        nativeEvent,
        nativeEventTarget,
        eventSystemFlags,
      );
      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }
  return events;
}

不一樣的事件類型會有不一樣的合成事件基類,而後再經過EventConstructor.getPooled生成事件,accumulateTwoPhaseDispatches用於獲取事件回調函數,最終調的是getListener方法。
爲了不頻繁建立和釋放事件對象致使性能損耗(對象建立和垃圾回收),React使用一個事件池來負責管理事件對象(在React17中再也不使用事件池機制),使用完的事件對象會放回池中,以備後續的複用,也就意味着事件處理器同步執行完後,SyntheticEvent屬性就會立刻被回收,不能訪問了,也就是事件中的e不能用了,若是要用的話,能夠經過一下兩種方式:

  • 使用e.persist(),告訴React不要回收對象池,在React17依舊能夠調用只是沒有實際做用。
  • 使用e. nativeEvent,由於它是持久引用的。

事件分發

事件分發就是遍歷找到當前元素及父元素全部綁定的事件,將全部的事件放到event._dispachListeners隊列中,以備後續的執行。

// packages\legacy-events\EventPropagators.js line 47
function accumulateDirectionalDispatches(inst, phase, event) {
  if (__DEV__) {
    warningWithoutStack(inst, 'Dispatching inst must not be null');
  }
  const listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    // 將提取到的綁定添加到_dispatchListeners中
    event._dispatchListeners = accumulateInto(
      event._dispatchListeners,
      listener,
    );
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

事件執行

執行事件隊列用到的方法是runEventsInBatch,遍歷執行executeDispatchesInOrder方法,經過executeDispatch執行調度,最終執行回調函數是經過invokeGuardedCallbackAndCatchFirstError方法。

// packages\legacy-events\EventBatching.js line 42
export function runEventsInBatch(
  events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }

  // Set `eventQueue` to null before processing it so that we can tell if more
  // events get enqueued while processing.
  const processingEventQueue = eventQueue;
  eventQueue = null;

  if (!processingEventQueue) {
    return;
  }

  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
  invariant(
    !eventQueue,
    'processEventQueue(): Additional events were enqueued while processing ' +
      'an event queue. Support for this has not yet been implemented.',
  );
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}

// packages\legacy-events\EventPluginUtils.js line 76
export function executeDispatchesInOrder(event) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;
  if (__DEV__) {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

// packages\legacy-events\EventPluginUtils.js line 66
export function executeDispatch(event, listener, inst) {
  const type = event.type || 'unknown-event';
  event.currentTarget = getNodeFromInstance(inst);
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

// packages\shared\ReactErrorUtils.js line 67
export function invokeGuardedCallbackAndCatchFirstError<
  A,
  B,
  C,
  D,
  E,
  F,
  Context,
>(
  name: string | null,
  func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
  context: Context,
  a: A,
  b: B,
  c: C,
  d: D,
  e: E,
  f: F,
): void {
  invokeGuardedCallback.apply(this, arguments);
  if (hasError) {
    const error = clearCaughtError();
    if (!hasRethrowError) {
      hasRethrowError = true;
      rethrowError = error;
    }
  }
}

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/53961511
https://zhuanlan.zhihu.com/p/25883536
https://zhuanlan.zhihu.com/p/140791931
https://www.jianshu.com/p/8d8f9aa4b033
https://toutiao.io/posts/28of14w/preview
https://juejin.cn/post/6844903988794671117
https://segmentfault.com/a/1190000015142568
https://zh-hans.reactjs.org/docs/events.html
https://github.com/UNDERCOVERj/tech-blog/issues/13
https://blog.csdn.net/kyooo0/article/details/111829693
相關文章
相關標籤/搜索