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 id
為4ab6305
,TAG
是React16.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
可返回某元素的根元素,在HTML
中HTML
文件本身是元素的根元素,所以可以說明其實大部分的事件都是註冊在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
繫結,將onBlur
、onFocus
、onScroll
等事件通過trapCapturedEvent
繫結,因為這些事件沒有冒泡行為,invalid
、submit
、reset
事件以及媒體等事件繫結到當前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
離散事件,例如blur
、focus
、click
、submit
、touchStart
,這些事件都是離散觸發的。UserBlockingEvent
使用者阻塞事件,例如touchMove
、mouseMove
、scroll
、drag
、dragOver
等等,這些事件會阻塞使用者的互動。ContinuousEvent
連續事件,例如load
、error
、loadStart
、abort
、animationEnd
,這個優先順序最高,也就是說它們應該是立即同步執行的,這就是Continuous
的意義,是持續地執行,不能被打斷。
此外React
將事件系統用到了Fiber
架構裡,Fiber
中將任務分成了5
大類,對應不同的優先順序,那麼三大類的事件系統和五大類的Fiber
任務系統的對應關係如下。
Immediate
: 此類任務會同步執行,或者說馬上執行且不能中斷,ContinuousEvent
便屬於此類。UserBlocking
: 此類任務一般是使用者互動的結果,需要及時得到反饋,DiscreteEvent
與UserBlockingEvent
都屬於此類。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,
);
}
}
在runExtractedPluginEventsInBatch
中extractPluginEvents
用於通過不同的外掛合成事件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
種外掛SimpleEventPlugin
、EnterLeaveEventPlugin
、ChangeEventPlugin
、SelectEventPlugin
、BeforeInputEventPlugin
。
// 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