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
,而不是onClick
。node
<!-- 剪贴板事件 --> 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>
简单来讲,在挂载的时候,经过listenerBank
把事件存起来了,触发的时候document
进行dispatchEvent
,找到触发事件的最深的一个节点,向上遍历拿到全部的callback
放在eventQueue
,根据事件类型构建event
对象,遍历执行eventQueue
,不简单点说,咱们能够查看一下React
对于事件处理的源码实现,commit id
为4ab6305
,TAG
是React16.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
可返回某元素的根元素,在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