React中的合成事件


React中的合成事件

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

描述

React的合成事件SyntheticEvent實際上就是React自己在內部實現的一套事件處理機制,它是瀏覽器的原生事件的跨瀏覽器包裝器,除兼容所有瀏覽器外,它還擁有和瀏覽器原生事件相同的接口,包括stopPropagation()preventDefault(),合成事件與瀏覽器的原生事件不同,也不會直接映射到原生事件,也就是說通常不要使用addEventListener為已創建的DOM元素添加監聽器,而應該直接使用React中定義的事件機制,而且在混用的情況下原生事件如果定義了阻止冒泡可能會阻止合成事件的執行,當然如果確實需要使用原生事件去處理需求,可以通過事件觸發傳遞的SyntheticEvent對象的nativeEvent屬性獲得原生Event對象的引用,React中的事件有以下幾個特點:

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

對於每個SyntheticEvent對象都包含以下屬性:

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,而不是onClick

<!-- 剪貼板事件 -->
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對象。

<!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,首先來看一下事件的處理流程。

/**
 * 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等,之后便是事件注冊、存儲、合成、分發、執行階段。

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

事件注冊

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

// 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM