React事件机制

Tags
Published
Author
在学习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)
notion image

React事件注册

事件注册是自执行的,也就是React自身进行调用的,过程如下:
React事件就是在组件中调用的onClick这种写法的事件。DOMpluginEventSystem.js中分为5个函数写,主要是区分不同的事件注册逻辑,但是最后都会添加到allNativeEventsSet数据结构中。

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开始, 调用链路比较长,最后调用addEventBubbleListeneraddEventCaptureListener监听了原生事件。
在注册原生事件的过程中,需要重点关注一下监听函数, 即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这个回调函数。
notion image
attemptToDispatchEvent把原生事件和fiber树关联起来。
核心逻辑:
  1. 定位原生 DOM 节点: 调用getEventTarget
  1. 获取与 DOM 节点对应的 fiber 节点: 调用getClosestInstanceFromNode
  1. 通过插件系统, 派发事件: 调用 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节点上的回调函数
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)
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); } // ...}
function processDispatchQueueItemsInOrder( event: ReactSyntheticEvent, dispatchListeners: Array<DispatchListener>, inCapturePhase: boolean,): void { let previousInstance; if (inCapturePhase) { // 1. capture事件: 倒序遍历listeners for (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事件: 顺序遍历listeners for (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=rootDOM元素上进行监听,并且对齐DOM元素fiber元素
  • 收集listeners: 遍历fiber树,收集所有监听本事件的listener函数。
  • 派发合成事件: 触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象,遍历listeners进行派发,执行代码中的回调函数。