在学习React的合成事件之前,重新提一下浏览器的事件系统和事件委托,这对理解React事件系统源码非常重要。
事件触发的顺序是从下至上,同一个元素上的事件按照绑定的顺序执行。
React16.X版本中,并不是将事件绑在该div的真实DOM上,而是在document处监听所有支持的事件,当 DOM 事件触发后冒泡到document,React 找到对应的组件,造一个 React 事件(SyntheticEvent)出来,并按组件树模拟一遍事件冒泡(此时原生 DOM 事件早已冒出document了)
从v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中,在这里我们主要看17.02版本的合成事件
为什么要实现合成事件
SyntheticEvent:是react内部创建的一个对象,是原生事件的跨浏览器包装器, 拥有和浏览器原生事件相同的接口(stopPropagation,preventDefault), 抹平不同浏览器 api 的差异,兼容性好。
1、解决了跨浏览器的兼容性问题
2、避免这类DOM事件滥用,如果DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响
合成事件的原理是什么?
- React 基于 Virtual DOM 实现了一个SyntheticEvent(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例,同样支持事件的冒泡机制,我们可以使用stopPropagation()和preventDefault()来中断它。
- 所有事件都自动绑定到最外层上(document)

具体到代码:ReactFiberCompleteWork.old.js
React事件注册
事件注册是自执行的,也就是React自身进行调用的,过程如下:
DOMpluginEventSystem.js
👇
registerSimpleEvents
👇
registerTwoPhaseEvent
👇
registerDirectEvent
👇
allNativeEvents
React事件就是在组件中调用的onClick这种写法的事件。DOMpluginEventSystem.js中分为5个函数写,主要是区分不同的事件注册逻辑,但是最后都会添加到allNativeEvents的Set数据结构中。
React事件绑定
在创建根Fiber节点之后,调用listenToAllSupportedEvents方法,其中rootContainerElement参数就是应用中的id = root的DOM元素。
这个函数主要遍历allNativeEvents, 调用listenToNativeEvent监听冒泡和捕获阶段的事件
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {if (enableEagerRootListeners) {(rootContainerElement: any)[listeningMarker] = true;allNativeEvents.forEach(domEventName => {if (!nonDelegatedEvents.has(domEventName)) {listenToNativeEvent(domEventName,false,((rootContainerElement: any): Element),null,);}listenToNativeEvent(domEventName,true,((rootContainerElement: any): Element),null,);});}}
export function listenToNativeEvent(domEventName: DOMEventName,isCapturePhaseListener: boolean,rootContainerElement: EventTarget,targetElement: Element | null,eventSystemFlags?: EventSystemFlags = 0,): void {let target = rootContainerElement;if (domEventName === 'selectionchange' &&(rootContainerElement: any).nodeType !== DOCUMENT_NODE) {target = (rootContainerElement: any).ownerDocument;}if (targetElement !== null &&!isCapturePhaseListener &&nonDelegatedEvents.has(domEventName)) {if (domEventName !== 'scroll') {return;}eventSystemFlags |= IS_NON_DELEGATED;target = targetElement;}const listenerSet = getEventListenerSet(target);const listenerSetKey = getListenerSetKey(domEventName,isCapturePhaseListener,);// If the listener entry is empty or we should upgrade, then// we need to trap an event listener onto the target.if (!listenerSet.has(listenerSetKey)) {if (isCapturePhaseListener) {eventSystemFlags |= IS_CAPTURE_PHASE;}addTrappedEventListener(target,domEventName,eventSystemFlags,isCapturePhaseListener,);listenerSet.add(listenerSetKey);}}
创建具有优先级的监听函数
function addTrappedEventListener(targetContainer: EventTarget,domEventName: DOMEventName,eventSystemFlags: EventSystemFlags,isCapturePhaseListener: boolean,isDeferredListenerForLegacyFBSupport?: boolean,) {let listener = createEventListenerWrapperWithPriority(targetContainer,domEventName,eventSystemFlags,);let isPassiveListener = undefined;if (passiveBrowserEventsSupported) {if (domEventName === 'touchstart' ||domEventName === 'touchmove' ||domEventName === 'wheel') {isPassiveListener = true;}}targetContainer =enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport? (targetContainer: any).ownerDocument: targetContainer;let unsubscribeListener;if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {const originalListener = listener;listener = function(...p) {removeEventListener(targetContainer,domEventName,unsubscribeListener,isCapturePhaseListener,);return originalListener.apply(this, p);};}// TODO: There are too many combinations here. Consolidate them.if (isCapturePhaseListener) {if (isPassiveListener !== undefined) {unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer,domEventName,listener,isPassiveListener,);} else {unsubscribeListener = addEventCaptureListener(targetContainer,domEventName,listener,);}} else {if (isPassiveListener !== undefined) {unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer,domEventName,listener,isPassiveListener,);} else {unsubscribeListener = addEventBubbleListener(targetContainer,domEventName,listener,);}}}
从listenToAllSupportedEvents开始, 调用链路比较长,最后调用addEventBubbleListener和addEventCaptureListener监听了原生事件。
4.createEventListenerWrapperWithPriority
在注册原生事件的过程中,需要重点关注一下监听函数, 即listener函数. 它实现了把原生事件派发到react体系之内, 非常关键:比如点击 DOM 触发原生事件, 原生事件最后会被派发到react内部的onClick函数。listener函数就是这个由外至内的关键环节。
和事件注册一样,listener也分为dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三种。它们之间的主要区别是执行优先级,还有discreteEvent涉及到要清除之前的discreteEvent问题,所以做了区分。但是它们最后都会调用dispatchEvent,最后绑定到div#root 上的这个统一的事件分发函数,其实就是 dispatchEvent。
export function createEventListenerWrapperWithPriority(targetContainer: EventTarget,domEventName: DOMEventName,eventSystemFlags: EventSystemFlags,): Function {const eventPriority = getEventPriorityForPluginSystem(domEventName);let listenerWrapper;switch (eventPriority) {case DiscreteEvent:listenerWrapper = dispatchDiscreteEvent;break;case UserBlockingEvent:listenerWrapper = dispatchUserBlockingUpdate;break;case ContinuousEvent:default:listenerWrapper = dispatchEvent;break;}return listenerWrapper.bind(null,domEventName,eventSystemFlags,targetContainer,);}
React事件触发
当原生事件触发之后,首先会进入到dispatchEvent这个回调函数。

attemptToDispatchEvent把原生事件和fiber树关联起来。
核心逻辑:
- 定位原生 DOM 节点: 调用getEventTarget
- 获取与 DOM 节点对应的 fiber 节点: 调用getClosestInstanceFromNode
- 通过插件系统, 派发事件: 调用 dispatchEventForPluginEventSystem
export function attemptToDispatchEvent(domEventName: DOMEventName,eventSystemFlags: EventSystemFlags,targetContainer: EventTarget,nativeEvent: AnyNativeEvent,): null | Container | SuspenseInstance {// 1. 定位原生DOM节点const nativeEventTarget = getEventTarget(nativeEvent);// 2. 获取与DOM节点对应的fiber节点let targetInst = getClosestInstanceFromNode(nativeEventTarget);..................// 3. 通过插件系统,派发事件dispatchEventForPluginEventSystem(domEventName,eventSystemFlags,nativeEvent,targetInst,targetContainer,);// We're not blocked on anything.return null;}
然后进行收集 fiber 上的 listener:
dispatchEvent函数的调用过程中, 通过不同的插件,处理不同的事件。其中最常见的事件都会由SimpleEventPlugin.extractEvents进行处理:
function extractEvents(dispatchQueue: DispatchQueue,domEventName: DOMEventName,targetInst: null | Fiber,nativeEvent: AnyNativeEvent,nativeEventTarget: null | EventTarget,eventSystemFlags: EventSystemFlags,targetContainer: EventTarget,): void {const reactName = topLevelEventsToReactNames.get(domEventName);if (reactName === undefined) {return;}let SyntheticEventCtor = SyntheticEvent;let reactEventType: string = domEventName;const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';// 1. 收集所有监听该事件的函数.const listeners = accumulateSinglePhaseListeners(targetInst,reactName,nativeEvent.type,inCapturePhase,accumulateTargetOnly,);if (listeners.length > 0) {// 2. 构造合成事件, 添加到派发队列const event = new SyntheticEventCtor(reactName,reactEventType,null,nativeEvent,nativeEventTarget,);dispatchQueue.push({ event, listeners });}}
收集所有listener回调,这里收集的是fiber.memoizedProps.onClick/onClickCapture等绑定在fiber节点上的回调函数
具体逻辑在accumulateSinglePhaseListeners:
export function accumulateSinglePhaseListeners(targetFiber: Fiber | null,reactName: string | null,nativeEventType: string,inCapturePhase: boolean,accumulateTargetOnly: boolean,): Array<DispatchListener> {const captureName = reactName !== null ? reactName + 'Capture' : null;const reactEventName = inCapturePhase ? captureName : reactName;const listeners: Array<DispatchListener> = [];let instance = targetFiber;let lastHostComponent = null;// 从targetFiber开始, 向上遍历, 直到 root 为止while (instance !== null) {const { stateNode, tag } = instance;// 当节点类型是HostComponent时(如: div, span, button等类型)if (tag === HostComponent && stateNode !== null) {lastHostComponent = stateNode;if (reactEventName !== null) {// 获取标准的监听函数 (如onClick , onClickCapture等)const listener = getListener(instance, reactEventName);if (listener != null) {listeners.push(createDispatchListener(instance, listener, lastHostComponent),);}}}// 如果只收集目标节点, 则不用向上遍历, 直接退出if (accumulateTargetOnly) {break;}instance = instance.return;}return listeners;}
收集完之后,进行构造合成事件(SyntheticEvent), 添加到派发队列(dispatchQueue)
当extractEvents完成之后, 在processDispatchQueue方法中执行真正的派发工作
export function processDispatchQueue(dispatchQueue: DispatchQueue,eventSystemFlags: EventSystemFlags,): void {const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;for (let i = 0; i < dispatchQueue.length; i++) {const { event, listeners } = dispatchQueue[i];processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);}// ...}
其中调用的processDispatchQueueItemsInOrder方法:
function processDispatchQueueItemsInOrder(event: ReactSyntheticEvent,dispatchListeners: Array<DispatchListener>,inCapturePhase: boolean,): void {let previousInstance;if (inCapturePhase) {// 1. capture事件: 倒序遍历listenersfor (let i = dispatchListeners.length - 1; i >= 0; i--) {const { instance, currentTarget, listener } = dispatchListeners[i];if (instance !== previousInstance && event.isPropagationStopped()) {return;}executeDispatch(event, listener, currentTarget);previousInstance = instance;}} else {// 2. bubble事件: 顺序遍历listenersfor (let i = 0; i < dispatchListeners.length; i++) {const { instance, currentTarget, listener } = dispatchListeners[i];if (instance !== previousInstance && event.isPropagationStopped()) {return;}executeDispatch(event, listener, currentTarget);previousInstance = instance;}}}
在processDispatchQueueItemsInOrder函数中遍历dispatchListeners数组,执行executeDispatch派发事件,在fiber节点上绑定的listener函数被执行。
在processDispatchQueueItemsInOrder函数中,根据捕获(captuer)或冒泡(bubble)的不同,采取了不同的遍历方式:
- capture事件: 从上至下调用fiber树中绑定的回调函数,所以倒序遍历dispatchListeners.。
- bubble事件: 从下至上调用fiber树中绑定的回调函数,所以顺序遍历dispatchListeners。
例子理解
export default class Test1 extends React.Component {innerClick = () => {console.log('A: react inner click.')}outerClick = () => {console.log('B: react outer click.')}componentDidMount() {document.getElementById('outer').addEventListener('click', () => {console.log('C: native outer click')})document.getElementById('inner').addEventListener('click', () => {console.log('D: native inner click')})}render() {return (<div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>BUTTON</button></div>)}}
在root上监听了 A:inner、B:outer
在div上监听了 C:outer
在button上监听了 D:inner
当点击button,从触发的节点冒泡,一次为DCAB
export default class Test2 extends React.Component {innerClick = (e) => {console.log('A: react inner click.')e.stopPropagation()}outerClick = () => {console.log('B: react outer click.')}componentDidMount() {document.getElementById('outer').addEventListener('click', () => {console.log('C: native outer click')})document.getElementById('inner').addEventListener('click', () => {console.log('D: native inner click')})}render() {return (<div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>BUTTON</button></div>)}}
在root上监听了 A:inner、B:outer
在div上监听了 C:outer
在button上监听了 D:inner
当点击button,从触发的节点冒泡,依次为DCA,由于调用了合成事件的方法stopPropagation,会阻止B的回调。
export default class Test4 extends React.Component {constructor(props) {super(props);}innerClick = (e) => {e.nativeEvent.stopImmediatePropagation()console.log("A: react inner click.");};outerClick = () => {console.log("B: react outer click.");};componentDidMount() {document.getElementById("root").addEventListener("click", () => {console.log("D: native document click");});}render() {return (<div id="outer" onClick={this.outerClick}><button id="inner" onClick={this.innerClick}>BUTTON</button></div>);}}
在root上监听了 A:inner、B:outer、D:native
点击BUTTON依次执行AB,冒泡依次执行的时候A中调用了nativeEvent.stopImmediatePropagation,会阻止事件在本元素中继续扩散,也就是会阻止D的原生事件。
这里为什么D在root的最后执行,原因是componentDidMount的时候才收集到这个函数。
总结
- 在React代码执行时,内部会自动执行事件的注册;
- 事件监听:第一次渲染,创建fiberRoot时,会进行原生事件的监听,所有的事件通过addEventListener委托在
id=root的DOM元素上进行监听,并且对齐DOM元素和fiber元素 - 收集listeners: 遍历fiber树,收集所有监听本事件的listener函数。
- 派发合成事件: 触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象,遍历listeners进行派发,执行代码中的回调函数。
