React事件機(jī)制源碼解析
React v17里事件機(jī)制有了比較大的改動(dòng),想來(lái)和v16差別還是比較大的。
本文淺析的React版本為17.0.1,使用ReactDOM.render創(chuàng)建應(yīng)用,不含優(yōu)先級(jí)相關(guān)。
原理簡(jiǎn)述
React中事件分為委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot創(chuàng)建的時(shí)候,就會(huì)在root節(jié)點(diǎn)的DOM元素上綁定幾乎所有事件的處理函數(shù),而不需要委托事件只會(huì)將處理函數(shù)綁定在DOM元素本身。
同時(shí),React將事件分為3種類型——discreteEvent、userBlockingEvent、continuousEvent,它們擁有不同的優(yōu)先級(jí),在綁定事件處理函數(shù)時(shí)會(huì)使用不同的回調(diào)函數(shù)。
React事件建立在原生基礎(chǔ)上,模擬了一套冒泡和捕獲的事件機(jī)制,當(dāng)某一個(gè)DOM元素觸發(fā)事件后,會(huì)冒泡到React綁定在root節(jié)點(diǎn)的處理函數(shù),通過(guò)target獲取觸發(fā)事件的DOM對(duì)象和對(duì)應(yīng)的Fiber節(jié)點(diǎn),由該Fiber節(jié)點(diǎn)向上層父級(jí)遍歷,收集一條事件隊(duì)列,再遍歷該隊(duì)列觸發(fā)隊(duì)列中每個(gè)Fiber對(duì)象對(duì)應(yīng)的事件處理函數(shù),正向遍歷模擬冒泡,反向遍歷模擬捕獲,所以合成事件的觸發(fā)時(shí)機(jī)是在原生事件之后的。
Fiber對(duì)象對(duì)應(yīng)的事件處理函數(shù)依舊是儲(chǔ)存在props里的,收集只是從props里取出來(lái),它并沒(méi)有綁定到任何元素上。
源碼淺析
以下源碼僅為基礎(chǔ)邏輯的淺析,旨在理清事件機(jī)制的觸發(fā)流程,去掉了很多流程無(wú)關(guān)或復(fù)雜的代碼。
委托事件綁定
這一步發(fā)生在調(diào)用了ReactDOM.render過(guò)程中,在創(chuàng)建fiberRoot的時(shí)候會(huì)在root節(jié)點(diǎn)的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
在綁定事件時(shí),會(huì)通過(guò)名為allNativeEvents的Set變量來(lái)獲取對(duì)應(yīng)的eventName,這個(gè)變量會(huì)在一個(gè)頂層函數(shù)進(jìn)行收集,而nonDelegatedEvents是一個(gè)預(yù)先定義好的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ù)在綁定事件之前會(huì)先將事件名在DOM元素中標(biāo)記,判斷為false時(shí)才會(huì)綁定。
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
// ...
// 在DOM元素上儲(chǔ)存一個(gè)Set用來(lái)標(biāo)識(shí)當(dāng)前元素監(jiān)聽了那些事件
const listenerSet = getEventListenerSet(target);
// 事件的標(biāo)識(shí)key,字符串拼接處理了下
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
if (!listenerSet.has(listenerSetKey)) {
// 標(biāo)記為捕獲
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// 綁定事件
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
// 添加到set
listenerSet.add(listenerSetKey);
}
}
addTrappedEventListener
addTrappedEventListener函數(shù)會(huì)通過(guò)事件名取得對(duì)應(yīng)優(yōu)先級(jí)的listener函數(shù),在交由下層函數(shù)處理事件綁定。
這個(gè)listener函數(shù)是一個(gè)閉包函數(shù),函數(shù)內(nèi)能訪問(wèn)targetContainer、domEventName、eventSystemFlags這三個(gè)變量。
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 根據(jù)優(yōu)先級(jí)取得對(duì)應(yīng)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來(lái)綁定事件了。
這一步是循環(huán)一個(gè)存有事件名的Set,將每一個(gè)事件對(duì)應(yīng)的處理函數(shù)綁定到root節(jié)點(diǎn)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方法里會(huì)綁定不需要委托的直接到DOM元素本身,也會(huì)設(shè)置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;
}
// 設(shè)置DOM屬性,如style...
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
}
switch里會(huì)根據(jù)不同的元素類型,綁定對(duì)應(yīng)的事件,這里只留下了video元素和audio元素的處理,它們會(huì)遍歷mediaEventTypes來(lái)將事件綁定在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ā)還是會(huì)去收集處理函數(shù)執(zhí)行。
事件處理函數(shù)
事件處理函數(shù)指的是React中的默認(rèn)處理函數(shù),并不是代碼里傳入的函數(shù)。
這個(gè)函數(shù)通過(guò)createEventListenerWrapperWithPriority方法創(chuàng)建,對(duì)應(yīng)的步驟在上文的addTrappedEventListener中。
createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 從內(nèi)置的Map中獲取事件優(yōu)先級(jí)
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
// 根據(jù)優(yōu)先級(jí)不同返回不同的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ù)里返回對(duì)應(yīng)事件優(yōu)先級(jí)的listener,這3個(gè)函數(shù)都接收4個(gè)參數(shù)。
function fn(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
//...
}
返回的時(shí)候bind了一下傳入了3個(gè)參數(shù),這樣返回的函數(shù)為只接收nativeEvent的處理函數(shù)了,但是能訪問(wèn)前3個(gè)參數(shù)。
dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法內(nèi)部其實(shí)都調(diào)用的dispatchEvent方法。
dispatchEvent
這里刪除了很多代碼,只看觸發(fā)事件的代碼。
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
// ...
// 觸發(fā)事件
attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
// ...
}
attemptToDispatchEvent方法里依然會(huì)處理很多復(fù)雜邏輯,同時(shí)函數(shù)調(diào)用棧也有幾層,我們就全部跳過(guò),只看關(guān)鍵的觸發(fā)函數(shù)。
dispatchEventsForPlugins
dispatchEventsForPlugins函數(shù)里會(huì)收集觸發(fā)事件開始各層級(jí)的節(jié)點(diǎn)對(duì)應(yīng)的處理函數(shù),也就是我們實(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í)行隊(duì)列
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents
extractEvents函數(shù)里主要是針對(duì)不同類型的事件創(chuàng)建對(duì)應(yīng)的合成事件,并且將各層級(jí)節(jié)點(diǎn)的listener收集起來(lái),用來(lái)模擬冒泡或者捕獲。
這里的代碼較長(zhǎng),刪除了不少無(wú)關(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ù)不同的事件來(lái)創(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;
}
// ...
// 收集各層級(jí)的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ù)里就是在向上層遍歷來(lái)收集一個(gè)列表后面會(huì)用來(lái)模擬冒泡。
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;
// 通過(guò)觸發(fā)事件的fiber節(jié)點(diǎn)向上層遍歷收集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é)點(diǎn)上的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 }]。
這個(gè)listeners則為一層一層收集到的數(shù)據(jù),類型為[{ currentTarget, instance, listener }]
processDispatchQueue
processDispatchQueue函數(shù)里會(huì)遍歷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中的每一項(xiàng)在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ù)里會(huì)根據(jù)判斷來(lái)正向、反向的遍歷來(lái)模擬冒泡和捕獲。
executeDispatch
executeDispatch函數(shù)里會(huì)執(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é)語(yǔ)
本文旨在理清事件機(jī)制的執(zhí)行,按照函數(shù)執(zhí)行棧簡(jiǎn)單的羅列了代碼邏輯,如果不對(duì)照代碼看是很難看明白的,原理在開篇就講述了。
React的事件機(jī)制隱晦而復(fù)雜,根據(jù)不同情況做了非常多的判斷,并且還有優(yōu)先級(jí)相關(guān)代碼、合成事件,這里都沒(méi)有一一講解,原因當(dāng)然是我還沒(méi)看~
平時(shí)用React也就寫寫簡(jiǎn)單的手機(jī)頁(yè)面,以前老板還經(jīng)常吐槽加載不夠快,那也沒(méi)啥辦法,就對(duì)我的工作而言,有沒(méi)有Cocurrent都是無(wú)關(guān)緊要的,這合成事件更復(fù)雜,完全就是不需要的,不過(guò)React的作者們腦洞還是牛皮,要是沒(méi)看源碼我肯定是想不到竟然模擬了一套事件機(jī)制。
小思考
- 為什么原生事件的stopPropagation可以阻止合成事件的傳遞?
這些問(wèn)題我放以前根本沒(méi)想過(guò),不過(guò)今天看了源碼以后才想的。
- 因?yàn)楹铣墒录窃谠录|發(fā)之后才開始收集并觸發(fā)的,所以當(dāng)原生事件調(diào)用stopPropagation阻止傳遞后,根本到不到root節(jié)點(diǎn),觸發(fā)不了React綁定的處理函數(shù),自然合成事件也不會(huì)觸發(fā),所以原生事件不是阻止了合成事件的傳遞,而是阻止了React中綁定的事件函數(shù)的執(zhí)行。
<div 原生onClick={(e)=>{e.stopPropagation()}}>
<div onClick={()=>{console.log("合成事件")}}>合成事件</div>
</div>
比如這個(gè)例子,在原生onClick阻止傳遞后,控制臺(tái)連“合成事件”這4個(gè)字都不會(huì)打出來(lái)了。
以上就是React事件機(jī)制源碼解析的詳細(xì)內(nèi)容,更多關(guān)于React事件機(jī)制源碼的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react學(xué)習(xí)筆記之state以及setState的使用
這篇文章主要介紹了react學(xué)習(xí)筆記之state以及setState的使用,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
React報(bào)錯(cuò)map()?is?not?a?function詳析
這篇文章主要介紹了React報(bào)錯(cuò)map()?is?not?a?function詳析,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
react寫一個(gè)select組件的實(shí)現(xiàn)代碼
這篇文章主要介紹了react寫一個(gè)select組件的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
React配置多個(gè)代理實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求返回問(wèn)題
這篇文章主要介紹了React之配置多個(gè)代理實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求返回問(wèn)題,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08
react滾動(dòng)加載useInfiniteScroll?詳解
使用useInfiniteScroll?hook可以處理檢測(cè)用戶何時(shí)滾動(dòng)到頁(yè)面底部的邏輯,并觸發(fā)回調(diào)函數(shù)以加載更多數(shù)據(jù),它還提供了一種簡(jiǎn)單的方法來(lái)管理加載和錯(cuò)誤消息的狀態(tài),今天通過(guò)實(shí)例代碼介紹下react滾動(dòng)加載useInfiniteScroll?相關(guān)知識(shí),感興趣的朋友跟隨小編一起看看吧2023-09-09

