React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的。
本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关。
原理简述
React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身。
同时,React将事件分为3种类型——discreteEvent、userBlockingEvent、continuousEvent,它们拥有不同的优先级,在绑定事件处理函数时会使用不同的回调函数。
React事件建立在原生基础上,模拟了一套冒泡和捕获的事件机制,当某一个DOM元素触发事件后,会冒泡到React绑定在root节点的处理函数,通过target获取触发事件的DOM对象和对应的Fiber节点,由该Fiber节点向上层父级遍历,收集一条事件队列,再遍历该队列触发队列中每个Fiber对象对应的事件处理函数,正向遍历模拟冒泡,反向遍历模拟捕获,所以合成事件的触发时机是在原生事件之后的。
Fiber对象对应的事件处理函数依旧是储存在props里的,收集只是从props里取出来,它并没有绑定到任何元素上。
源码浅析
以下源码仅为基础逻辑的浅析,旨在理清事件机制的触发流程,去掉了很多流程无关或复杂的代码。
委托事件绑定
这一步发生在调用了ReactDOM.render过程中,在创建fiberRoot的时候会在root节点的DOM元素上监听所有支持的事件。
function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) { // ... const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container; // 监听所有支持的事件 listenToAllSupportedEvents(rootContainerElement); // ... }
listenToAllSupportedEvents
在绑定事件时,会通过名为allNativeEvents的Set变量来获取对应的eventName,这个变量会在一个顶层函数进行收集,而nonDelegatedEvents是一个预先定义好的Set。
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) { allNativeEvents.forEach(domEventName => { // 排除不需要委托的事件 if (!nonDelegatedEvents.has(domEventName)) { // 冒泡 listenToNativeEvent( domEventName, false, ((rootContainerElement: any): Element), null, ); } // 捕获 listenToNativeEvent( domEventName, true, ((rootContainerElement: any): Element), null, ); }); }
listenToNativeEvent
listenToNativeEvent函数在绑定事件之前会先将事件名在DOM元素中标记,判断为false时才会绑定。
export function listenToNativeEvent( domEventName: DOMEventName, isCapturePhaseListener: boolean, rootContainerElement: EventTarget, targetElement: Element | null, eventSystemFlags?: EventSystemFlags = 0, ): void { let target = rootContainerElement; // ... // 在DOM元素上储存一个Set用来标识当前元素监听了那些事件 const listenerSet = getEventListenerSet(target); // 事件的标识key,字符串拼接处理了下 const listenerSetKey = getListenerSetKey( domEventName, isCapturePhaseListener, ); if (!listenerSet.has(listenerSetKey)) { // 标记为捕获 if (isCapturePhaseListener) { eventSystemFlags |= IS_CAPTURE_PHASE; } // 绑定事件 addTrappedEventListener( target, domEventName, eventSystemFlags, isCapturePhaseListener, ); // 添加到set listenerSet.add(listenerSetKey); } }
addTrappedEventListener
addTrappedEventListener函数会通过事件名取得对应优先级的listener函数,在交由下层函数处理事件绑定。
这个listener函数是一个闭包函数,函数内能访问targetContainer、domEventName、eventSystemFlags这三个变量。
function addTrappedEventListener( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport?: boolean, ) { // 根据优先级取得对应listener let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, ); if (isCapturePhaseListener) { addEventCaptureListener(targetContainer, domEventName, listener); } else { addEventBubbleListener(targetContainer, domEventName, listener); } }
addEventCaptureListener函数和addEventBubbleListener函数内部就是调用原生的target.addEventListener来绑定事件了。
这一步是循环一个存有事件名的Set,将每一个事件对应的处理函数绑定到root节点DOM元素上。
不需要委托事件绑定
不需要委托的事件其中也包括媒体元素的事件。
export const nonDelegatedEvents: Set<DOMEventName> = new Set([ 'cancel', 'close', 'invalid', 'load', 'scroll', 'toggle', ...mediaEventTypes, ]); export const mediaEventTypes: Array<DOMEventName> = [ 'abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting', ];
setInitialProperties
setInitialProperties方法里会绑定不需要委托的直接到DOM元素本身,也会设置style和一些传入的DOM属性。
export function setInitialProperties( domElement: Element, tag: string, rawProps: Object, rootContainerElement: Element | Document, ): void { let props: Object; switch (tag) { // ... case 'video': case 'audio': for (let i = 0; i < mediaEventTypes.length; i++) { listenToNonDelegatedEvent(mediaEventTypes[i], domElement); } props = rawProps; break; default: props = rawProps; } // 设置DOM属性,如style... setInitialDOMProperties( tag, domElement, rootContainerElement, props, isCustomComponentTag, ); }
switch里会根据不同的元素类型,绑定对应的事件,这里只留下了video元素和audio元素的处理,它们会遍历mediaEventTypes来将事件绑定在DOM元素本身上。
listenToNonDelegatedEvent
listenToNonDelegatedEvent方法逻辑和上一节的listenToNativeEvent方法基本一致。
export function listenToNonDelegatedEvent( domEventName: DOMEventName, targetElement: Element, ): void { const isCapturePhaseListener = false; const listenerSet = getEventListenerSet(targetElement); const listenerSetKey = getListenerSetKey( domEventName, isCapturePhaseListener, ); if (!listenerSet.has(listenerSetKey)) { addTrappedEventListener( targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener, ); listenerSet.add(listenerSetKey); } }
值得注意的是,虽然事件处理绑定在DOM元素本身,但是绑定的事件处理函数不是代码中传入的函数,后续触发还是会去收集处理函数执行。
事件处理函数
事件处理函数指的是React中的默认处理函数,并不是代码里传入的函数。
这个函数通过createEventListenerWrapperWithPriority方法创建,对应的步骤在上文的addTrappedEventListener中。
createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, ): Function { // 从内置的Map中获取事件优先级 const eventPriority = getEventPriorityForPluginSystem(domEventName); let listenerWrapper; // 根据优先级不同返回不同的listener 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, ); }
createEventListenerWrapperWithPriority函数里返回对应事件优先级的listener,这3个函数都接收4个参数。
function fn( domEventName, eventSystemFlags, container, nativeEvent, ) { //... }
返回的时候bind了一下传入了3个参数,这样返回的函数为只接收nativeEvent的处理函数了,但是能访问前3个参数。
dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法内部其实都调用的dispatchEvent方法。
dispatchEvent
这里删除了很多代码,只看触发事件的代码。
export function dispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent, ): void { // ... // 触发事件 attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); // ... }
attemptToDispatchEvent方法里依然会处理很多复杂逻辑,同时函数调用栈也有几层,我们就全部跳过,只看关键的触发函数。
dispatchEventsForPlugins
dispatchEventsForPlugins函数里会收集触发事件开始各层级的节点对应的处理函数,也就是我们实际传入JSX中的函数,并且执行它们。
function dispatchEventsForPlugins( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, targetContainer: EventTarget, ): void { const nativeEventTarget = getEventTarget(nativeEvent); const dispatchQueue: DispatchQueue = []; // 收集listener模拟冒泡 extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); // 执行队列 processDispatchQueue(dispatchQueue, eventSystemFlags); }
extractEvents
extractEvents函数里主要是针对不同类型的事件创建对应的合成事件,并且将各层级节点的listener收集起来,用来模拟冒泡或者捕获。
这里的代码较长,删除了不少无关代码。
function extractEvents( dispatchQueue: DispatchQueue, domEventName: DOMEventName, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, ): void { const reactName = topLevelEventsToReactNames.get(domEventName); let SyntheticEventCtor = SyntheticEvent; let reactEventType: string = domEventName; // 根据不同的事件来创建不同的合成事件 switch (domEventName) { case 'keypress': case 'keydown': case 'keyup': SyntheticEventCtor = SyntheticKeyboardEvent; break; case 'click': // ... case 'mouseover': SyntheticEventCtor = SyntheticMouseEvent; break; case 'drag': // ... case 'drop': SyntheticEventCtor = SyntheticDragEvent; break; // ... default: break; } // ... // 收集各层级的listener const listeners = accumulateSinglePhaseListeners( targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly, ); if (listeners.length > 0) { // 创建合成事件 const event = new SyntheticEventCtor( reactName, reactEventType, null, nativeEvent, nativeEventTarget, ); dispatchQueue.push({event, listeners}); } }
accumulateSinglePhaseListeners
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; // 通过触发事件的fiber节点向上层遍历收集dom和listener while (instance !== null) { const {stateNode, tag} = instance; // 只有HostComponents有listener (i.e. <div>) if (tag === HostComponent && stateNode !== null) { lastHostComponent = stateNode; if (reactEventName !== null) { // 从fiber节点上的props中获取传入的事件listener函数 const listener = getListener(instance, reactEventName); if (listener != null) { listeners.push({ instance, listener, currentTarget: lastHostComponent, }); } } } if (accumulateTargetOnly) { break; } // 继续向上 instance = instance.return; } return listeners; }
最后的数据结构如下:
dispatchQueue的数据结构为数组,类型为[{ event,listeners }]。
这个listeners则为一层一层收集到的数据,类型为[{ currentTarget, instance, listener }]
processDispatchQueue
processDispatchQueue函数里会遍历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); } }
dispatchQueue中的每一项在processDispatchQueueItemsInOrder函数里遍历执行。
processDispatchQueueItemsInOrder
function processDispatchQueueItemsInOrder( event: ReactSyntheticEvent, dispatchListeners: Array<DispatchListener>, inCapturePhase: boolean, ): void { let previousInstance; // 捕获 if (inCapturePhase) { 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 { // 冒泡 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函数里会根据判断来正向、反向的遍历来模拟冒泡和捕获。
executeDispatch
executeDispatch函数里会执行listener。
function executeDispatch( event: ReactSyntheticEvent, listener: Function, currentTarget: EventTarget, ): void { const type = event.type || 'unknown-event'; event.currentTarget = currentTarget; listener(event); event.currentTarget = null; }
结语
本文旨在理清事件机制的执行,按照函数执行栈简单的罗列了代码逻辑,如果不对照代码看是很难看明白的,原理在开篇就讲述了。
React的事件机制隐晦而复杂,根据不同情况做了非常多的判断,并且还有优先级相关代码、合成事件,这里都没有一一讲解,原因当然是我还没看~
平时用React也就写写简单的手机页面,以前老板还经常吐槽加载不够快,那也没啥办法,就对我的工作而言,有没有Cocurrent都是无关紧要的,这合成事件更复杂,完全就是不需要的,不过React的作者们脑洞还是牛皮,要是没看源码我肯定是想不到竟然模拟了一套事件机制。
小思考
- 为什么原生事件的stopPropagation可以阻止合成事件的传递?
这些问题我放以前根本没想过,不过今天看了源码以后才想的。
- 因为合成事件是在原生事件触发之后才开始收集并触发的,所以当原生事件调用stopPropagation阻止传递后,根本到不到root节点,触发不了React绑定的处理函数,自然合成事件也不会触发,所以原生事件不是阻止了合成事件的传递,而是阻止了React中绑定的事件函数的执行。
<div 原生onClick={(e)=>{e.stopPropagation()}}> <div onClick={()=>{console.log("合成事件")}}>合成事件</div> </div>
比如这个例子,在原生onClick阻止传递后,控制台连“合成事件”这4个字都不会打出来了。
以上就是React事件机制源码解析的详细内容,更多关于React事件机制源码的资料请关注自学编程网其它相关文章!
- 本文固定链接: https://zxbcw.cn/post/209191/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)