1. 程式人生 > >React中的合成事件

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 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
``` ### 示例 一個簡單的示例,同時繫結在一個`DOM`上的原生事件與`React`事件,因為原生事件阻止冒泡而導致`React`事件無法執行,同時我們也可以看到`React`傳遞的`event`並不是原生`Event`物件的例項,而是`React`自行實現維護的一個`event`物件。 ``` React ``` ## 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`列表中,在的話便註冊事件,列表包含了可以註冊的事件。 ```javascript // 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`方法實際註冊。 ```javascript // 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`上。 ```javascript // 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, ): 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`方法。 ```javascript // 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`之前會需要進行一些特殊的處理。 ```javascript // 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`上註冊了各種事件。 ```javascript // 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`節點作為鍵名。 ```javascript // packages\react-dom\src\events\ReactBrowserEventEmitter.js line 88 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; const elementListeningSets: | WeakMap | Map< Document | Element | Node, Set, > = new PossiblyWeakMap(); export function getListeningSetForElement( element: Document | Element | Node, ): Set { let listeningSet = elementListeningSets.get(element); if (listeningSet === undefined) { listeningSet = new Set(); elementListeningSets.set(element, listeningSet); } return listeningSet; } ``` ### 事件合成 首先來看看`handleTopLevel`的邏輯,`handleTopLevel`主要是快取祖先元素,避免事件觸發後找不到祖先元素報錯,接下來就進入`runExtractedPluginEventsInBatch`方法。 ```javascript // 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).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`則是完成事件的觸發。 ```javascript // 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`。 ```javascript // packages\legacy-events\EventPluginHub.js line 133 function extractPluginEvents( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, eventSystemFlags: EventSystemFlags, ): Array | 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 = 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`佇列中,以備後續的執行。 ```javascript // 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`方法。 ```javascript // packages\legacy-events\EventBatching.js line 42 export function runEventsInBatch( events: Array | 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/111829