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事件系统初看比较复杂,其实理解后也并无那么难。在解决跨平台和兼容性的问题时,保持了高性能,有不少值得学习的地方。在看源代码的时候,一开始也没有头绪,多打断点,一点点调试,也就慢慢理解。文中若有不正确的地方,还望不吝指正。