React實現本身封裝了一套事件系統,基本原理爲將全部的事件都代理到頂層元素上(如documen元素)上進行處理,帶來的好處有:javascript
本文基於React 16.8.1
在詳細講解以前,先思考幾個問題,能夠幫助咱們更好理解React的事件系統。html
React事件系統與原生事件混用的執行順序問題html5
class App extends React.Component { handleWrapperCaptureClick() { console.log('wrapper capture click') } handleButtonClick() { console.log('button click') } componentDidMount() { const buttonEle = document.querySelector('#btn') buttonEle.addEventListener('click', () => { console.log('button native click') }) window.addEventListener('click', () => { console.log('window native click') }) } render() { <div className="wrapper" onClickCapture={this.handleWrapperCaptureClick}> <button id="btn" onClick={this.handleButtonClick}> click me </button> </div> } }
異步回調中獲取事件對象失敗問題java
handleClick(e) { fetch('/a/b/c').then(() => { console.log(e) }) }
若是看完本文後,能清晰的回答出這幾個問題,說明你對React事件系統已經有比較清楚的理解了。下面就正式進入正文了。node
事件綁定在/packages/react-dom/src/client/ReactDOMComponent.js
文件中react
} else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { ensureListeningTo(rootContainerElement, propKey); } }
若是propkey是registrationNameModules中的一個事件名,則經過ensureListeningTo方法綁定,其中registrationNameModules爲包含React全部事件一個的map,在事件plugin部分中會再提到。數組
function ensureListeningTo(rootContainerElement, registrationName) { const isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; const doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument; listenTo(registrationName, doc); }
從ensureListeningTo方法中能夠看出,React事件掛載在document節點或者DocumentFragment上,listenTo方法則是真正將事件註冊的入口,截取部分代碼以下:瀏覽器
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. isListening[TOP_BLUR] = true; isListening[TOP_FOCUS] = true; break; case TOP_CANCEL: case TOP_CLOSE: if (isEventSupported(getRawEventName(dependency))) { trapCapturedEvent(dependency, 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: // By default, listen on the top level to all non-media events. // Media events don't bubble so adding the listener wouldn't do anything. const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; if (!isMediaEvent) { trapBubbledEvent(dependency, mountAt); } break;
部分特殊事件作單獨處理,默認將事件經過trapBubbledEvent放到綁定,trapBubbledEvent根據字面意思可知就是綁定到冒泡事件上。其中注意的是blur等事件是經過trapCapturedEvent綁定的,這是由於blur等方法不支持冒泡事件,可是支持捕獲事件,因此須要使用trapCapturedEvent綁定。緩存
接下來咱們看下trapBubbledEvent方法。app
function trapBubbledEvent( topLevelType: DOMTopLevelEventType, element: Document | Element, ) { if (!element) { return null; } const dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; addEventBubbleListener( element, getRawEventName(topLevelType), // Check if interactive and wrap in interactiveUpdates dispatch.bind(null, topLevelType), ); }
trapBubbledEvent就是將事件經過addEventBubbleListener綁定到document上的。dispatch則是事件的回調函數。dispatchInteractiveEvent和dispatchEvent的區別爲,dispatchInteractiveEvent在執行前會確保以前全部的任務都已執行,具體見/packages/react-reconciler/src/ReactFiberScheduler.js
中的interactiveUpdates方法,該模塊不是本文討論的重點,感興趣能夠本身看看。
事件的綁定已經介紹完畢,下面介紹事件的合成及觸發,該部分爲React事件系統的核心。
事件在dispatch方法中將事件的相關信息保存到bookKeeping中,其中bookKeeping也有個bookKeeping池,從而避免了反覆建立銷燬變量致使瀏覽器頻繁GC。
建立完bookkeeping後就傳入handleTopLevel處理了,handleTopLevel主要是緩存祖先元素,避免事件觸發後找不到祖先元素報錯。接下來就進入runExtractedEventsInBatch方法了。
function runExtractedEventsInBatch( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, ) { const events = extractEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, ); runEventsInBatch(events); }
runExtractedEventsInBatch代碼很短,可是很是重要,其中extractEvents經過不一樣插件合成事件,runEventsInBatch則是完成事件的觸發,事件觸發放到下一小節中再講,接下來先講事件的合成。
function extractEvents( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, ): 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, ); if (extractedEvents) { events = accumulateInto(events, extractedEvents); } } } return events; }
能夠看到extractEvents經過遍歷全部插件的extractEvents方法合成事件,若是一個插件適用該事件,則返回一個events,不然返回爲null,意味着最後產生的events有多是個數組。每一個插件至少有兩部分組成:eventTypes和extractEvents,eventTypes會在初始化的時候生成前文提到的registrationNameModules,extractEvents用於合成事件。下面介紹SimpleEventPlugin和ChangeEventPlugin兩個插件。
插件是在初始化的時候經過EventPluginHubInjection插入的,並對其進行排序等初始化工做,不一樣的平臺會注入不一樣的插件。
const SimpleEventPlugin: PluginModule<MouseEvent> & { isInteractiveTopLevelEventType: (topLevelType: TopLevelType) => boolean, } = { eventTypes: eventTypes, isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean { const config = topLevelEventsToDispatchConfig[topLevelType]; return config !== undefined && config.isInteractive === true; }, extractEvents: function( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: MouseEvent, nativeEventTarget: EventTarget, ): null | ReactSyntheticEvent { const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { return null; } let EventConstructor; switch (topLevelType) { case DOMTopLevelEventTypes.TOP_KEY_PRESS: // Firefox creates a keypress event for function keys too. This removes // the unwanted keypress events. Enter is however both printable and // non-printable. One would expect Tab to be as well (but it isn't). if (getEventCharCode(nativeEvent) === 0) { return null; } /* falls through */ case DOMTopLevelEventTypes.TOP_KEY_DOWN: case DOMTopLevelEventTypes.TOP_KEY_UP: EventConstructor = SyntheticKeyboardEvent; break; case DOMTopLevelEventTypes.TOP_BLUR: case DOMTopLevelEventTypes.TOP_FOCUS: EventConstructor = SyntheticFocusEvent; break; case DOMTopLevelEventTypes.TOP_CLICK: // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return null; } /* falls through */ case DOMTopLevelEventTypes.TOP_AUX_CLICK: case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK: case DOMTopLevelEventTypes.TOP_MOUSE_DOWN: case DOMTopLevelEventTypes.TOP_MOUSE_MOVE: case DOMTopLevelEventTypes.TOP_MOUSE_UP: /* falls through */ case DOMTopLevelEventTypes.TOP_MOUSE_OUT: case DOMTopLevelEventTypes.TOP_MOUSE_OVER: case DOMTopLevelEventTypes.TOP_CONTEXT_MENU: EventConstructor = SyntheticMouseEvent; break; case DOMTopLevelEventTypes.TOP_DRAG: case DOMTopLevelEventTypes.TOP_DRAG_END: case DOMTopLevelEventTypes.TOP_DRAG_ENTER: case DOMTopLevelEventTypes.TOP_DRAG_EXIT: case DOMTopLevelEventTypes.TOP_DRAG_LEAVE: case DOMTopLevelEventTypes.TOP_DRAG_OVER: case DOMTopLevelEventTypes.TOP_DRAG_START: case DOMTopLevelEventTypes.TOP_DROP: EventConstructor = SyntheticDragEvent; break; case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL: case DOMTopLevelEventTypes.TOP_TOUCH_END: case DOMTopLevelEventTypes.TOP_TOUCH_MOVE: case DOMTopLevelEventTypes.TOP_TOUCH_START: EventConstructor = SyntheticTouchEvent; break; case DOMTopLevelEventTypes.TOP_ANIMATION_END: case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION: case DOMTopLevelEventTypes.TOP_ANIMATION_START: EventConstructor = SyntheticAnimationEvent; break; case DOMTopLevelEventTypes.TOP_TRANSITION_END: EventConstructor = SyntheticTransitionEvent; break; case DOMTopLevelEventTypes.TOP_SCROLL: EventConstructor = SyntheticUIEvent; break; case DOMTopLevelEventTypes.TOP_WHEEL: EventConstructor = SyntheticWheelEvent; break; case DOMTopLevelEventTypes.TOP_COPY: case DOMTopLevelEventTypes.TOP_CUT: case DOMTopLevelEventTypes.TOP_PASTE: EventConstructor = SyntheticClipboardEvent; break; case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE: case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE: case DOMTopLevelEventTypes.TOP_POINTER_CANCEL: case DOMTopLevelEventTypes.TOP_POINTER_DOWN: case DOMTopLevelEventTypes.TOP_POINTER_MOVE: case DOMTopLevelEventTypes.TOP_POINTER_OUT: case DOMTopLevelEventTypes.TOP_POINTER_OVER: case DOMTopLevelEventTypes.TOP_POINTER_UP: EventConstructor = SyntheticPointerEvent; break; default: // HTML Events // @see http://www.w3.org/TR/html5/index.html#events-0 EventConstructor = SyntheticEvent; break; } const event = EventConstructor.getPooled( dispatchConfig, targetInst, nativeEvent, nativeEventTarget, ); accumulateTwoPhaseDispatches(event); return event; }, };
能夠看到不一樣的事件類型會有不一樣的合成事件基類,而後再經過EventConstructor.getPooled生成事件。在default中的SyntheticEvent咱們能夠看到熟悉的preventDefault、stopPropagation、persist等方法,其中有個persist須要說明下,由上文可知事件對象會循環使用,因此一個事件完成後事件就會被回收,所以在異步回調中是拿不到事件的,而調用persist方法後會保持事件的引用不被回收。preventDefault則調用原生事件的preventDefault方法,並標記isDefaultPrevented,該屬性下一節會再繼續講。
合成事件以後,會經過accumulateTwoPhaseDispatches收集父級事件監聽並儲存到_dispatchListeners中,這裏是React事件系統模擬冒泡的關鍵。
export function traverseTwoPhase(inst, fn, arg) { const path = []; // 遍歷父級元素 while (inst) { path.push(inst); inst = getParent(inst); } let i; // 分別放入捕獲和冒泡隊列中 // fn爲accumulateDirectionalDispatches方法 for (i = path.length; i-- > 0; ) { fn(path[i], 'captured', arg); } for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); } }
function accumulateDirectionalDispatches(inst, phase, event) { // 提取綁定的監聽事件 const listener = listenerAtPhase(inst, event, phase); if (listener) { // 將提取到的綁定添加到_dispatchListeners中 event._dispatchListeners = accumulateInto( event._dispatchListeners, listener, ); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } }
const ChangeEventPlugin = { eventTypes: eventTypes, _isInputEventSupported: isInputEventSupported, extractEvents: function( topLevelType, targetInst, nativeEvent, nativeEventTarget, ) { const targetNode = targetInst ? getNodeFromInstance(targetInst) : window; let getTargetInstFunc, handleEventFunc; if (shouldUseChangeEvent(targetNode)) { getTargetInstFunc = getTargetInstForChangeEvent; } else if (isTextInputElement(targetNode)) { if (isInputEventSupported) { getTargetInstFunc = getTargetInstForInputOrChangeEvent; } else { getTargetInstFunc = getTargetInstForInputEventPolyfill; handleEventFunc = handleEventsForInputEventPolyfill; } } else if (shouldUseClickEvent(targetNode)) { getTargetInstFunc = getTargetInstForClickEvent; } if (getTargetInstFunc) { const inst = getTargetInstFunc(topLevelType, targetInst); if (inst) { const event = createAndAccumulateChangeEvent( inst, nativeEvent, nativeEventTarget, ); return event; } } if (handleEventFunc) { handleEventFunc(topLevelType, targetNode, targetInst); } // When blurring, set the value attribute for number inputs if (topLevelType === TOP_BLUR) { handleControlledInputBlur(targetNode); } }, };
MDN中對change事件有如下描述:
事件觸發取決於表單元素的類型(type)和用戶對標籤的操做:
- 當元素被:checked時(經過點擊或者使用鍵盤):<input type="radio"> 和 <input type="checkbox">;
- 當用戶完成提交動做時(例如:點擊了 <select>中的一個選項,從 <input type="date">標籤選擇了一個日期,經過<input type="file">標籤上傳了一個文件,等);
- 當標籤的值被修改而且失焦後,但並未進行提交(例如:對<textarea> 或者<input type="text">的值進行編輯後。)。
ChangeEventPlugin中shouldUseChangeEvent對應的<input type="date">與<input type="file">元素,監聽change事件;isTextInputElement對應普通input元素,監聽input事件;shouldUseClickEvent對應<input type="radio">與<input type="checkbox">元素,監聽click事件。
因此普通input元素中當時區焦點後纔會觸發change事件,而React的change事件在每次輸入的時候都會觸發,由於監聽的是input事件。
截止到目前已經完成了事件的綁定與合成,接下來就是最後一步事件的觸發了。事件觸發的入口爲前文提到的runEventsInBatch方法,該方法中會遍歷觸發合成的事件。
function executeDispatchesInOrder(event) { const dispatchListeners = event._dispatchListeners; const dispatchInstances = event._dispatchInstances; // 遍歷觸發dispatchListeners中收集的事件 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; }
其中event.isPropagationStopped()
爲判斷是否須要阻止冒泡,須要注意的是由於是代理到document上的,原生事件早已冒泡到了document上,因此stopPropagation是沒法阻止原生事件的冒泡,只能阻止React事件的冒泡。executeDispatch
就是最終觸發回調事件的地方,並捕獲錯誤。至此React事件的綁定、合成與觸發都已經結束了。
React事件系統初看比較複雜,其實理解後也並無那麼難。在解決跨平臺和兼容性的問題時,保持了高性能,有不少值得學習的地方。在看源代碼的時候,一開始也沒有頭緒,多打斷點,一點點調試,也就慢慢理解。文中若有不正確的地方,還望不吝指正。