React事件機制源碼解析
React v17里事件機制有了比較大的改動,想來和v16差別還是比較大的。
本文淺析的React版本為17.0.1,使用ReactDOM.render創(chuàng)建應用,不含優(yōu)先級相關(guān)。
原理簡述
React中事件分為委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot創(chuàng)建的時候,就會在root節(jié)點的DOM元素上綁定幾乎所有事件的處理函數(shù),而不需要委托事件只會將處理函數(shù)綁定在DOM元素本身。
同時,React將事件分為3種類型——discreteEvent、userBlockingEvent、continuousEvent,它們擁有不同的優(yōu)先級,在綁定事件處理函數(shù)時會使用不同的回調(diào)函數(shù)。
React事件建立在原生基礎上,模擬了一套冒泡和捕獲的事件機制,當某一個DOM元素觸發(fā)事件后,會冒泡到React綁定在root節(jié)點的處理函數(shù),通過target獲取觸發(fā)事件的DOM對象和對應的Fiber節(jié)點,由該Fiber節(jié)點向上層父級遍歷,收集一條事件隊列,再遍歷該隊列觸發(fā)隊列中每個Fiber對象對應的事件處理函數(shù),正向遍歷模擬冒泡,反向遍歷模擬捕獲,所以合成事件的觸發(fā)時機是在原生事件之后的。
Fiber對象對應的事件處理函數(shù)依舊是儲存在props里的,收集只是從props里取出來,它并沒有綁定到任何元素上。
源碼淺析
以下源碼僅為基礎邏輯的淺析,旨在理清事件機制的觸發(fā)流程,去掉了很多流程無關(guān)或復雜的代碼。
委托事件綁定
這一步發(fā)生在調(diào)用了ReactDOM.render過程中,在創(chuàng)建fiberRoot的時候會在root節(jié)點的DOM元素上監(jiān)聽所有支持的事件。
function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) { // ... const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container; // 監(jiān)聽所有支持的事件 listenToAllSupportedEvents(rootContainerElement); // ... }
listenToAllSupportedEvents
在綁定事件時,會通過名為allNativeEvents的Set變量來獲取對應的eventName,這個變量會在一個頂層函數(shù)進行收集,而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函數(shù)在綁定事件之前會先將事件名在DOM元素中標記,判斷為false時才會綁定。
export function listenToNativeEvent( domEventName: DOMEventName, isCapturePhaseListener: boolean, rootContainerElement: EventTarget, targetElement: Element | null, eventSystemFlags?: EventSystemFlags = 0, ): void { let target = rootContainerElement; // ... // 在DOM元素上儲存一個Set用來標識當前元素監(jiān)聽了那些事件 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函數(shù)會通過事件名取得對應優(yōu)先級的listener函數(shù),在交由下層函數(shù)處理事件綁定。
這個listener函數(shù)是一個閉包函數(shù),函數(shù)內(nèi)能訪問targetContainer、domEventName、eventSystemFlags這三個變量。
function addTrappedEventListener( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport?: boolean, ) { // 根據(jù)優(yōu)先級取得對應listener let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, ); if (isCapturePhaseListener) { addEventCaptureListener(targetContainer, domEventName, listener); } else { addEventBubbleListener(targetContainer, domEventName, listener); } }
addEventCaptureListener函數(shù)和addEventBubbleListener函數(shù)內(nèi)部就是調(diào)用原生的target.addEventListener來綁定事件了。
這一步是循環(huán)一個存有事件名的Set,將每一個事件對應的處理函數(shù)綁定到root節(jié)點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里會根據(jù)不同的元素類型,綁定對應的事件,這里只留下了video元素和audio元素的處理,它們會遍歷mediaEventTypes來將事件綁定在DOM元素本身上。
listenToNonDelegatedEvent
listenToNonDelegatedEvent方法邏輯和上一節(jié)的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元素本身,但是綁定的事件處理函數(shù)不是代碼中傳入的函數(shù),后續(xù)觸發(fā)還是會去收集處理函數(shù)執(zhí)行。
事件處理函數(shù)
事件處理函數(shù)指的是React中的默認處理函數(shù),并不是代碼里傳入的函數(shù)。
這個函數(shù)通過createEventListenerWrapperWithPriority方法創(chuàng)建,對應的步驟在上文的addTrappedEventListener中。
createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, ): Function { // 從內(nèi)置的Map中獲取事件優(yōu)先級 const eventPriority = getEventPriorityForPluginSystem(domEventName); let listenerWrapper; // 根據(jù)優(yōu)先級不同返回不同的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函數(shù)里返回對應事件優(yōu)先級的listener,這3個函數(shù)都接收4個參數(shù)。
function fn( domEventName, eventSystemFlags, container, nativeEvent, ) { //... }
返回的時候bind了一下傳入了3個參數(shù),這樣返回的函數(shù)為只接收nativeEvent的處理函數(shù)了,但是能訪問前3個參數(shù)。
dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法內(nèi)部其實都調(diào)用的dispatchEvent方法。
dispatchEvent
這里刪除了很多代碼,只看觸發(fā)事件的代碼。
export function dispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent, ): void { // ... // 觸發(fā)事件 attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); // ... }
attemptToDispatchEvent方法里依然會處理很多復雜邏輯,同時函數(shù)調(diào)用棧也有幾層,我們就全部跳過,只看關(guān)鍵的觸發(fā)函數(shù)。
dispatchEventsForPlugins
dispatchEventsForPlugins函數(shù)里會收集觸發(fā)事件開始各層級的節(jié)點對應的處理函數(shù),也就是我們實際傳入JSX中的函數(shù),并且執(zhí)行它們。
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, ); // 執(zhí)行隊列 processDispatchQueue(dispatchQueue, eventSystemFlags); }
extractEvents
extractEvents函數(shù)里主要是針對不同類型的事件創(chuàng)建對應的合成事件,并且將各層級節(jié)點的listener收集起來,用來模擬冒泡或者捕獲。
這里的代碼較長,刪除了不少無關(guān)代碼。
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; // 根據(jù)不同的事件來創(chuàng)建不同的合成事件 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) { // 創(chuàng)建合成事件 const event = new SyntheticEventCtor( reactName, reactEventType, null, nativeEvent, nativeEventTarget, ); dispatchQueue.push({event, listeners}); } }
accumulateSinglePhaseListeners
accumulateSinglePhaseListeners函數(shù)里就是在向上層遍歷來收集一個列表后面會用來模擬冒泡。
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; // 通過觸發(fā)事件的fiber節(jié)點向上層遍歷收集dom和listener while (instance !== null) { const {stateNode, tag} = instance; // 只有HostComponents有l(wèi)istener (i.e. <div>) if (tag === HostComponent && stateNode !== null) { lastHostComponent = stateNode; if (reactEventName !== null) { // 從fiber節(jié)點上的props中獲取傳入的事件listener函數(shù) const listener = getListener(instance, reactEventName); if (listener != null) { listeners.push({ instance, listener, currentTarget: lastHostComponent, }); } } } if (accumulateTargetOnly) { break; } // 繼續(xù)向上 instance = instance.return; } return listeners; }
最后的數(shù)據(jù)結(jié)構(gòu)如下:
dispatchQueue的數(shù)據(jù)結(jié)構(gòu)為數(shù)組,類型為[{ event,listeners }]。
這個listeners則為一層一層收集到的數(shù)據(jù),類型為[{ currentTarget, instance, listener }]
processDispatchQueue
processDispatchQueue函數(shù)里會遍歷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函數(shù)里遍歷執(zhí)行。
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函數(shù)里會根據(jù)判斷來正向、反向的遍歷來模擬冒泡和捕獲。
executeDispatch
executeDispatch函數(shù)里會執(zhí)行l(wèi)istener。
function executeDispatch( event: ReactSyntheticEvent, listener: Function, currentTarget: EventTarget, ): void { const type = event.type || 'unknown-event'; event.currentTarget = currentTarget; listener(event); event.currentTarget = null; }
結(jié)語
本文旨在理清事件機制的執(zhí)行,按照函數(shù)執(zhí)行棧簡單的羅列了代碼邏輯,如果不對照代碼看是很難看明白的,原理在開篇就講述了。
React的事件機制隱晦而復雜,根據(jù)不同情況做了非常多的判斷,并且還有優(yōu)先級相關(guān)代碼、合成事件,這里都沒有一一講解,原因當然是我還沒看~
平時用React也就寫寫簡單的手機頁面,以前老板還經(jīng)常吐槽加載不夠快,那也沒啥辦法,就對我的工作而言,有沒有Cocurrent都是無關(guān)緊要的,這合成事件更復雜,完全就是不需要的,不過React的作者們腦洞還是牛皮,要是沒看源碼我肯定是想不到竟然模擬了一套事件機制。
小思考
- 為什么原生事件的stopPropagation可以阻止合成事件的傳遞?
這些問題我放以前根本沒想過,不過今天看了源碼以后才想的。
- 因為合成事件是在原生事件觸發(fā)之后才開始收集并觸發(fā)的,所以當原生事件調(diào)用stopPropagation阻止傳遞后,根本到不到root節(jié)點,觸發(fā)不了React綁定的處理函數(shù),自然合成事件也不會觸發(fā),所以原生事件不是阻止了合成事件的傳遞,而是阻止了React中綁定的事件函數(shù)的執(zhí)行。
<div 原生onClick={(e)=>{e.stopPropagation()}}> <div onClick={()=>{console.log("合成事件")}}>合成事件</div> </div>
比如這個例子,在原生onClick阻止傳遞后,控制臺連“合成事件”這4個字都不會打出來了。
以上就是React事件機制源碼解析的詳細內(nèi)容,更多關(guān)于React事件機制源碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React報錯map()?is?not?a?function詳析
這篇文章主要介紹了React報錯map()?is?not?a?function詳析,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-08-08React配置多個代理實現(xiàn)數(shù)據(jù)請求返回問題
這篇文章主要介紹了React之配置多個代理實現(xiàn)數(shù)據(jù)請求返回問題,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08