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元素上監聽所有支持的事件。
1
2
3
4
5
6
7
8
9
10
11
12
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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時才會綁定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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這三個變量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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元素上。
不需要委托事件綁定
不需要委托的事件其中也包括媒體元素的事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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屬性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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方法基本一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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個參數。
1
2
3
4
5
6
7
8
|
function fn( domEventName, eventSystemFlags, container, nativeEvent, ) { //... } |
返回的時候bind了一下傳入了3個參數,這樣返回的函數為只接收nativeEvent的處理函數了,但是能訪問前3個參數。
dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法內部其實都調用的dispatchEvent方法。
dispatchEvent
這里刪除了很多代碼,只看觸發事件的代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
export function dispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent, ): void { // ... // 觸發事件 attemptToDispatchEvent( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); // ... } |
attemptToDispatchEvent方法里依然會處理很多復雜邏輯,同時函數調用棧也有幾層,我們就全部跳過,只看關鍵的觸發函數。
dispatchEventsForPlugins
dispatchEventsForPlugins函數里會收集觸發事件開始各層級的節點對應的處理函數,也就是我們實際傳入JSX中的函數,并且執行它們。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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收集起來,用來模擬冒泡或者捕獲。
這里的代碼較長,刪除了不少無關代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
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函數里就是在向上層遍歷來收集一個列表后面會用來模擬冒泡。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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。
1
2
3
4
5
6
7
8
9
10
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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。
1
2
3
4
5
6
7
8
9
10
|
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中綁定的事件函數的執行。
1
2
3
|
<div 原生onClick={(e)=>{e.stopPropagation()}}> <div onClick={()=>{console.log( "合成事件" )}}>合成事件</div> </div> |
比如這個例子,在原生onClick阻止傳遞后,控制臺連“合成事件”這4個字都不會打出來了。
以上就是React事件機制源碼解析的詳細內容,更多關于React事件機制源碼的資料請關注服務器之家其它相關文章!
原文鏈接:https://juejin.cn/post/6948726117591154719