React 事件系統(tǒng)addTrappedEventListener的使用
React 合成事件系統(tǒng)最底層 DOM 監(jiān)聽器創(chuàng)建與綁定核心函數(shù)
- 生成 React 封裝的高優(yōu)先級事件回調(diào)
- 自動區(qū)分 touch/wheel 事件開啟 passive 優(yōu)化
- 根據(jù)捕獲 / 冒泡 + 是否 passive 分流調(diào)用對應底層綁定 API
- 生成解綁句柄,供一次性監(jiān)聽自動銷毀
完整調(diào)用鏈路全覽
──────────────────────────────────────────────────────────────────────
createRoot(container)
──────────────────────────────────────────────────────────────────────
│
▼
listenToAllSupportedEvents(container)
│
├── (container._reactListening = true)
│
├── allNativeEvents.forEach('click'):
│ ├── 'click' ? nonDelegatedEvents
│ │ └── listenToNativeEvent('click', false, container)
│ │ └── addTrappedEventListener(container, 'click', 0, false)
│ │ ├── createEventListenerWrapperWithPriority(container, 'click', 0)
│ │ │ └── getEventPriority('click') → DiscreteEventPriority
│ │ │ └── dispatchDiscreteEvent.bind(null, 'click', 0, container)
│ │ ├── passiveBrowserEventsSupported? Yes, but click ∈ [touchstart,touchmove,wheel]? No
│ │ │ └── isPassiveListener = undefined
│ │ ├── isCapturePhaseListener? false → addEventBubbleListener
│ │ │ └── container.addEventListener('click', wrapper, false)
│ │ └── return listener (unsubscribe)
│ │
│ └── listenToNativeEvent('click', true, container)
│ └── addTrappedEventListener(container, 'click', IS_CAPTURE_PHASE, true)
│ ├── dispatchDiscreteEvent.bind(null, 'click', IS_CAPTURE_PHASE, container)
│ ├── isCapturePhaseListener? true → addEventCaptureListener
│ │ └── container.addEventListener('click', wrapper, true)
│ └── return listener
│
├── allNativeEvents.forEach('touchstart'):
│ ├── listenToNativeEvent('touchstart', false, container)
│ │ └── addTrappedEventListener(container, 'touchstart', 0, false)
│ │ ├── isPassiveListener = true (domEventName === 'touchstart')
│ │ ├── isCapturePhaseListener? false, isPassiveListener? true
│ │ │ └── addEventBubbleListenerWithPassiveFlag(container, 'touchstart', wrapper, true)
│ │ │ └── container.addEventListener('touchstart', wrapper, { passive: true })
│ │ └── return listener
│ │
│ └── listenToNativeEvent('touchstart', true, container)
│ └── addTrappedEventListener(container, 'touchstart', IS_CAPTURE_PHASE, true)
│ ├── isCapturePhaseListener? true, isPassiveListener? true
│ │ └── addEventCaptureListenerWithPassiveFlag(container, 'touchstart', wrapper, true)
│ │ └── container.addEventListener('touchstart', wrapper, { capture: true, passive: true })
│ └── return listener
│
└── ownerDocument = document
└── listenToNativeEvent('selectionchange', false, document)
└── addTrappedEventListener(document, 'selectionchange', 0, false)
├── getEventPriority('selectionchange') → DiscreteEventPriority
├── isPassiveListener? false (non-passive)
└── document.addEventListener('selectionchange', wrapper, false)
──────────────────────────────────────────────────────────────────────
mount(<img onLoad={fn} />)
──────────────────────────────────────────────────────────────────────
│
└── ReactDOMComponent.finalizeInitialChildren
└── listenToNonDelegatedEvent('load', imgElement)
├── listenerSet = imgElement.__reactEvents || new Set()
├── listenerSetKey = "load__bubble"
├── Set has "load__bubble"? No
│
└── addTrappedEventListener(imgElement, 'load', IS_NON_DELEGATED, false)
├── createEventListenerWrapperWithPriority(imgElement, 'load', IS_NON_DELEGATED)
│ └── getEventPriority('load') → DefaultEventPriority
│ └── dispatchEvent.bind(null, 'load', IS_NON_DELEGATED, imgElement)
├── isPassiveListener = undefined (load ? [touchstart,touchmove,wheel])
└── imgElement.addEventListener('load', wrapper, false)
└── listenerSet.add("load__bubble")源碼解析
步驟 ①:創(chuàng)建優(yōu)先級包裝器
生成非原生、帶 React 調(diào)度能力的攔截回調(diào):原生 DOM 觸發(fā)事件時,不會直接執(zhí)行組件 onClick,而是先走這個 wrapper。把原生 DOM 事件與 ``React 內(nèi)部渲染調(diào)度、合成事件機制解耦,所有底層監(jiān)聽統(tǒng)一使用標準化包裝函數(shù),上層不用關心調(diào)度細節(jié)
let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, );
步驟 ②:被動事件優(yōu)化
瀏覽器支持 passive 配置時,滾動 / 觸摸類事件強制設為 passive: true。自動做滾動性能優(yōu)化,開發(fā)者無需手動配置,抹平瀏覽器滾動卡頓缺陷
let isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}
// 特性檢測:瀏覽器是否支持 { passive: true } 選項
if (canUseDOM) {
try {
const options = {};
Object.defineProperty(options, 'passive', {
get: function () {
passiveBrowserEventsSupported = true; // getter 觸發(fā) → 瀏覽器支持
},
});
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (e) {
passiveBrowserEventsSupported = false;
}
}
檢測原理
addEventListener 第三個參數(shù)可以傳入 { passive: true }。如果瀏覽器支持,options.passive 的 getter 會被觸發(fā),passiveBrowserEventsSupported 設為 true。如果瀏覽器不支持此選項(如 IE ≤ 11),catch 塊設為 false
設計意義
Chrome 56+ 將 touchstart/touchmove/wheel 的 document 級監(jiān)聽器默認設為 { passive: true }(因為開發(fā)者幾乎不會在 touch 事件上調(diào)用 preventDefault 但必須等待 JS 執(zhí)行判斷是否調(diào)用)。React 19 不把事件綁到 document 而綁到 rootContainer,手動模擬這個 passive 行為以獲得相同性能收益
步驟 ③:選擇 addEventListener 重載
根據(jù)捕獲 / 冒泡 + 是否 passive 分流到 4 個底層綁定工具,統(tǒng)一管理 DOM addEventListener 參數(shù)差異,保存解綁函數(shù),支撐一次性監(jiān)聽自動移除,為后續(xù)全局卸載根容器事件提供基礎能力
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,
);
}
}
export function addEventBubbleListener(
target,
eventType,
listener,
) {
target.addEventListener(eventType, listener, false);
return listener;
}
export function addEventCaptureListener(
target,
eventType,
listener,
) {
target.addEventListener(eventType, listener, true);
return listener;
}
export function addEventCaptureListenerWithPassiveFlag(
target,
eventType,
listener,
passive,
) {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
export function addEventBubbleListenerWithPassiveFlag(
target,
eventType,
listener,
passive,
) {
target.addEventListener(eventType, listener, {
passive,
});
return listener;
}
四種組合
- 捕獲 + passive(touch/wheel)
- 捕獲 + 無 passive(普通事件如 clickCapture)
- 冒泡 + passive(touch/wheel 常規(guī)監(jiān)聽)
- 冒泡 + 無 passive(常規(guī) click/input 等)
| isCapturePhaseListener | isPassiveListener | 調(diào)用的函數(shù) | DOM 參數(shù) |
|---|---|---|---|
| true | true | addEventCaptureListenerWithPassiveFlag | { capture: true, passive: true } |
| true | undefined | addEventCaptureListener | true |
| false | true | addEventBubbleListenerWithPassiveFlag | { passive: true } |
| false | undefined | addEventBubbleListener | false |
輔助函數(shù)
創(chuàng)建優(yōu)先級事件監(jiān)聽器:createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
) {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
作用
- 調(diào)用 getEventPriority 根據(jù)事件名稱區(qū)分事件優(yōu)先級,React 并發(fā)渲染調(diào)度的核心依據(jù)
- DiscreteEventPriority:離散高優(yōu)事件(click、keydown、input、focus 等,用戶即時交互,需要同步響應,可打斷渲染)
- ContinuousEventPriority:連續(xù)高頻事件(mousemove、scroll、touchmove 等,持續(xù)觸發(fā),低打斷權(quán)重)
- DefaultEventPriority:默認普通優(yōu)先級(其余事件)
- 按事件類型分流調(diào)度邏輯,實現(xiàn) React 并發(fā)渲染的優(yōu)先級機制,不同交互行為擁有不同渲染搶占能力
- 離散高優(yōu)事件:使用 dispatchDiscreteEvent。標記離散更新、同步刷新、允許中斷后臺渲染,保證點擊、輸入即時響應
- 連續(xù)高頻事件:使用 dispatchContinuousEvent。走連續(xù)調(diào)度,合并多次更新,避免高頻觸發(fā)造成頁面卡頓
- 默認 / 其他事件:兜底 dispatchEvent,標準合成事件流程
- bind 預綁定固定參數(shù),返回最終監(jiān)聽回調(diào):把靜態(tài)不變的配置(事件名、捕獲標記、根容器)提前固化,不用每次事件觸發(fā)再傳遞 / 查找,減少運行時計算開銷;同時統(tǒng)一分發(fā)函數(shù)入?yún)⒁?guī)范,下層分發(fā)邏輯無需額外從 DOM 讀取信息
- 預固化三個前置參數(shù):domEventName、eventSystemFlags、targetContainer
- 當瀏覽器觸發(fā)事件執(zhí)行回調(diào)時,原生 Event 對象會作為第四個參數(shù)傳入分發(fā)函數(shù)
事件優(yōu)先級映射(getEventPriority)
| 優(yōu)先級 | 代表事件 | 包裝器 |
|---|---|---|
| DiscreteEventPriority | click, keydown, focusin, input, mousedown, touchstart, submit, toggle, beforetoggle, cancel, close, paste, copy, cut, contextmenu, auxclick, dblclick, dragend, dragstart, drop, invalid, keypress, keyup, mouseup, pause, play, pointercancel, pointerdown, pointerup, ratechange, reset, seeked, touchcancel, touchend, volumechange, change, selectionchange, beforeblur, afterblur, blur, focus, hashchange, popstate, select, selectstart, beforeinput, fullscreenchange, fullscreenerror | dispatchDiscreteEvent |
| ContinuousEventPriority | mousemove, mouseout, mouseover, pointermove, pointerout, pointerover, drag, dragenter, dragexit, dragleave, dragover, resize, scroll, touchmove, wheel, mouseenter, mouseleave, pointerenter, pointerleave | dispatchContinuousEvent |
| DefaultEventPriority | load, error, animationend, transitionend, message(特定調(diào)度優(yōu)先級)等不匹配上述分類的事件 | dispatchEvent |
離散事件的調(diào)度包裝器:dispatchDiscreteEvent
- 臨時提升渲染優(yōu)先級,保障交互即時性:針對 click/input/keydown 等離散用戶交互,把事件內(nèi)所有狀態(tài)更新強制設為最高優(yōu)先級,能夠打斷后臺正在執(zhí)行的低優(yōu)先級渲染,解決點擊延遲、輸入卡頓問題,是 React 并發(fā)渲染交互流暢性的核心保障
- 隔離 startTransition 低優(yōu)上下文:強制清空全局 Transition 標記,確保離散交互不受 startTransition 降級邏輯影響,緊急操作永遠優(yōu)先渲染
- 上下文壓棧 + 自動恢復,無全局污染:采用先緩存現(xiàn)場 → 修改全局環(huán)境 → 執(zhí)行事件邏輯 → 無論成敗強制還原的范式,不會篡改外部代碼的優(yōu)先級、Transition 環(huán)境,多個事件嵌套、并發(fā)執(zhí)行時上下文互不干擾
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
設計意義
即使當前代碼處在 startTransition 內(nèi)部,點擊 / 輸入這類緊急交互也要走高優(yōu)渲染,不能被降級延遲,事件執(zhí)行過程中產(chǎn)生的所有 setState 更新,都會標記為高優(yōu),可以中斷正在進行的后臺渲染,保證交互即時響應
連續(xù)事件的調(diào)度包裝器:dispatchContinuousEvent
- 設置連續(xù)事件專屬調(diào)度優(yōu)先級,平衡流暢性與性能針對滾動、鼠標移動、拖拽、滾輪等每秒多次觸發(fā)的高頻事件,使用中等優(yōu)先級:
- 不會像離散點擊那樣強行搶占渲染;
- 多個連續(xù)更新會被調(diào)度器合并,減少重復渲染開銷;
- 當同時發(fā)生點擊輸入時,離散事件可以打斷滾動渲染,保證用戶主動交互優(yōu)先
- 剝離 startTransition 低優(yōu)上下文:清空全局 Transition 標記,滾動相關更新不會被當作延遲渲染任務,避免滾動滯后、畫面掉幀
- 安全隔離全局調(diào)度環(huán)境,無副作用污染:采用緩存現(xiàn)場 → 臨時覆蓋 → 最終強制還原的棧式上下文模式,單次事件執(zhí)行僅局部生效,函數(shù)退出后全局狀態(tài)完全復原,異常場景也不會破壞全局調(diào)度規(guī)則
function dispatchContinuousEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
try {
setCurrentUpdatePriority(ContinuousEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
設計意義
滾動、鼠標移動這類持續(xù)交互,不降級為 transition 更新,使用專屬連續(xù)優(yōu)先級(優(yōu)先級低于點擊、輸入等離散事件),后臺渲染任務遇到離散交互會被直接搶占中斷,連續(xù)事件之間會合并調(diào)度,減少重復渲染,防止高頻事件卡死頁面。高頻滾動事件不搶占主線程,避免短時間大量渲染阻塞頁面
事件分發(fā)中樞控制器:dispatchEvent
針對 SSR 水合阻塞場景做分流處理
- 無未水合 DOM:直接執(zhí)行完整事件流程
- 高頻連續(xù)事件遇阻塞:暫存隊列、丟棄本次觸發(fā)
- 捕獲階段離散交互事件遇阻塞:同步強制水合組件,再執(zhí)行事件
- 其余普通阻塞場景:直接執(zhí)行事件(不等待水合)
export function dispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
// 標記 React 合成事件系統(tǒng)是否啟用;關閉則直接跳過所有事件處理
if (!_enabled) {
return;
}
// 查找當前觸發(fā)事件的 DOM 節(jié)點鏈上,還未完成水合(hydrate)的 DOM 元素;返回對應 DOM,無阻塞節(jié)點返回 null
let blockedOn = findInstanceBlockingEvent(nativeEvent);
if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst, // 事件目標 DOM 對應的 Fiber 實例
targetContainer,
);
// 清空當前連續(xù)事件緩存隊列,丟棄過期事件
clearIfContinuousEvent(domEventName, nativeEvent);
return;
}
if (
// 連續(xù)事件(scroll/mousemove/touchmove)入隊
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
clearIfContinuousEvent(domEventName, nativeEvent);
if (
eventSystemFlags & IS_CAPTURE_PHASE &&
// 判斷該離散事件是否必須等待水合完成才能執(zhí)行(click、input、keydown 等用戶強交互)
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = findInstanceBlockingEvent(nativeEvent);
if (nextBlockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
}
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn !== null) {
nativeEvent.stopPropagation();
}
return;
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
作用
- 事件系統(tǒng)總開關攔截:全局關閉合成事件系統(tǒng)時,直接終止事件分發(fā),不執(zhí)行任何后續(xù)邏輯
- 調(diào)用 findInstanceBlockingEvent 查找是否存在未水合阻塞 DOM:從原生事件觸發(fā)目標向上遍歷 DOM 樹,查找對應 Fiber 未完成水合的 DOM 節(jié)點
- 無任何未水合阻塞節(jié)點(正常流程):直接執(zhí)行事件,傳入目標 Fiber,清空連續(xù)事件隊列
- 調(diào)用核心事件執(zhí)行函數(shù) dispatchEventForPluginEventSystem,傳入目標 DOM 對應的 Fiber 實例 return_targetInst,完整執(zhí)行合成事件、遍歷 Fiber、執(zhí)行用戶回調(diào)
- clearIfContinuousEvent:清空該事件類型的連續(xù)事件緩存隊列,直接退出
- 存在阻塞節(jié)點,優(yōu)先處理高頻連續(xù)事件:事件入隊延遲執(zhí)行,停止冒泡,放棄本次分發(fā),不阻塞主線程
- queueIfContinuousEvent 判斷當前是否為 scroll/mousemove/touchmove 等連續(xù)高頻事件
- 是連續(xù)事件:將本次事件存入延遲隊列,等待水合完成后統(tǒng)一批量執(zhí)行;調(diào)用 nativeEvent.stopPropagation 停止原生事件冒泡,不繼續(xù)向下分發(fā),直接退出
- 非連續(xù)事件:執(zhí)行 clearIfContinuousEvent 清空歷史緩存
- queueIfContinuousEvent 判斷當前是否為 scroll/mousemove/touchmove 等連續(xù)高頻事件
- 捕獲階段 + 存在阻塞需要強制水合的離散交互事件(click/input 等):同步強制水合所有阻塞組件,水合完成后執(zhí)行事件,若水合失敗則停止冒泡,保證即時響應
- 根據(jù)阻塞 DOM 拿到對應 Fiber
- attemptSynchronousHydration:同步阻塞主線程,強制完成該組件水合
- 重新查找阻塞節(jié)點 nextBlockedOn
- 如果水合后無阻塞節(jié)點,直接執(zhí)行完整事件分發(fā)
- 如果前后阻塞節(jié)點不變,代表無法完成水合,跳出循環(huán)
- 更新 blockedOn,循環(huán)處理下一個阻塞 DOM
- 循環(huán)結(jié)束后,若仍存在未水合節(jié)點,停止原生事件冒泡,直接退出
- 兜底分支:普通冒泡離散事件,存在阻塞但無需同步水合:不阻塞主線程,直接執(zhí)行事件,動態(tài)查找 Fiber 兼容半水合狀態(tài)
設計意義
SSR/SSG頁面:HTML先渲染到頁面,JS加載完成后才會執(zhí)行水合;此時點擊DOM,對應組件還未掛載Fiber,無法讀取事件回調(diào),需要特殊兼容- 丟棄過期高頻過渡事件:懸浮、焦點、拖拽類事件觸發(fā)頻率極高,水合階段積壓大量事件會造成水合完成后主線程卡頓;執(zhí)行有效事件后清空緩存,減少后續(xù)批量執(zhí)行開銷,提升
SSR頁面交互流暢度 - 高頻滾動類事件不需要同步阻塞等待水合,入隊延遲執(zhí)行,避免主線程卡頓;同時停止冒泡,防止原生事件繼續(xù)擴散
- 捕獲階段的用戶點擊、輸入是強即時交互,不能延遲;因此強制同步水合組件,保證事件回調(diào)可以正常執(zhí)行,解決
SSR頁面點擊失效問題 - 普通冒泡離散事件不阻塞主線程同步水合,直接嘗試執(zhí)行事件;框架內(nèi)部動態(tài)兼容半水合狀態(tài),平衡交互響應速度與主線程阻塞開銷
查找未水合阻塞 DOM:findInstanceBlockingEvent
- 專門識別三類會導致事件失效的特殊容器:脫水根、Suspense、Activity
- 判定是否存在水合阻塞容器,存在則返回阻塞 DOM;
- 緩存事件目標對應的有效 Fiber 到全局變量,供事件插件系統(tǒng)復用,避免重復查找 DOM-Fiber 映射
- 當事件觸發(fā)節(jié)點自身未掛載,僅上層祖先掛載時,清空 Fiber 實例,避免事件系統(tǒng)讀取未就緒組件的回調(diào),防止報錯
export function findInstanceBlockingEvent(
nativeEvent,
) {
const nativeEventTarget = getEventTarget(nativeEvent);
return findInstanceBlockingTarget(nativeEventTarget);
}
function getEventTarget(nativeEvent) {
let target = nativeEvent.target || nativeEvent.srcElement || window;
if (target.correspondingUseElement) {
target = target.correspondingUseElement;
}
return target.nodeType === TEXT_NODE ? target.parentNode : target;
}
export let return_targetInst = null;
export function findInstanceBlockingTarget(
targetNode,
) {
return_targetInst = null;
// 從 DOM 向上找最近綁定 Fiber 的實例
let targetInst = getClosestInstanceFromNode(targetNode);
if (targetInst !== null) {
// 從當前 Fiber 向上找最近已掛載的 Fiber
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
targetInst = null;
} else {
const tag = nearestMounted.tag;
// Suspense 組件,子樹延遲加載,內(nèi)部 DOM 已渲染但未激活
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
return instance;
}
targetInst = null;
// 離屏緩存組件(React 內(nèi)部),DOM 存在但組件未激活
} else if (tag === ActivityComponent) {
const instance = getActivityInstanceFromFiber(nearestMounted);
if (instance !== null) {
return instance;
}
targetInst = null;
// 應用根 Fiber,對應 #root 容器
} else if (tag === HostRoot) {
const root = nearestMounted.stateNode;
if (isRootDehydrated(root)) {
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
targetInst = null;
}
}
}
return_targetInst = targetInst;
return null;
}
作用
- 獲取事件真實觸發(fā) DOM:統(tǒng)一抹平瀏覽器事件 target 相關 API 差異,正確取出事件真正觸發(fā)的 DOM 節(jié)點,拿到事件源頭 DOM,作為向上遍歷查找阻塞節(jié)點的起點
- 轉(zhuǎn)發(fā)給核心查找函數(shù) findInstanceBlockingTarget
- 調(diào)用 getClosestInstanceFromNode 獲取當前 DOM 綁定的最近 Fiber:從傳入的 DOM 節(jié)點向上遍歷,拿到綁定在 DOM 上的 Fiber 實例
- 存在 Fiber 實例,調(diào)用 getNearestMountedFiber,從當前 Fiber 向上遍歷 Fiber 樹,找到第一個已完成掛載的祖先 Fiber
- 無任何已掛載祖先 Fiber:整條組件樹都未掛載,不存在可響應事件的 Fiber,清空 targetInst,后續(xù)直接返回 null
- 最近掛載節(jié)點是 Suspense
- getSuspenseInstanceFromFiber 獲取 Suspense 對應的 DOM 容器
- 容器存在 → 直接返回該 DOM,代表被 Suspense 阻塞水合
- 無容器則清空 targetInst,視為無阻塞
- 最近掛載節(jié)點是 Activity 離屏組件:DOM 保留在頁面但組件處于凍結(jié) / 未激活狀態(tài),事件無法正常響應,找到對應 DOM 容器直接返回,標記為阻塞節(jié)點
- 最近掛載節(jié)點是應用根 HostRoot,判斷根容器是否處于脫水未水合狀態(tài)(SSR 剛加載完 HTML,JS 還沒完成水合)
- 脫水狀態(tài):返回根容器 DOM,代表全局水合阻塞
- 已完成水合:清空 targetInst
- 最近掛載 Fiber 不是當前事件節(jié)點自身:當前觸發(fā)事件的 Fiber 自身未掛載,僅上層祖先掛載;組件未就緒,不能作為有效事件目標,清空 targetInst
- 把有效 Fiber 存入全局變量供上層使用:走完所有阻塞判斷后,若 targetInst 不為 null,代表當前 DOM 對應的組件已正常掛載、無阻塞;存入全局變量,后續(xù)插件事件系統(tǒng)可以直接拿到事件目標 Fiber,快速收集事件處理器
- 函數(shù)統(tǒng)一返回 null:只有上面 Suspense/Activity/ 脫水根三個分支會提前 return DOM 阻塞節(jié)點;其余所有正常、無阻塞場景統(tǒng)一返回 null
清理緩存的過渡類事件:clearIfContinuousEvent
清理 SSR 半水合階段積壓的過渡類事件緩存,最終只執(zhí)行最新一次交互事件,中間滑動產(chǎn)生的過渡事件全部作廢。當頁面處于半水合狀態(tài)時,queueIfContinuousEvent 會把頻繁觸發(fā)的懸浮、焦點、拖拽、指針事件存入全局隊列延遲執(zhí)行;一旦當前事件可以正常分發(fā)(組件已水合、無阻塞),調(diào)用本函數(shù)清空對應緩存,避免過期事件后續(xù)補發(fā)造成狀態(tài)錯亂
- 本次焦點事件正常分發(fā)執(zhí)行,清空之前緩存的焦點隊列,丟棄未執(zhí)行的舊焦點事件
- 清空拖拽事件緩存,過期拖拽事件不再補發(fā)
- 清空鼠標懸浮緩存
- 只刪除當前觸點對應的緩存事件,不影響其他觸點隊列(指針設備支持多觸點,用
pointerId區(qū)分不同手指 / 觸控筆) - 按觸點
ID清除對應指針捕獲緩存事件
export function clearIfContinuousEvent(
domEventName,
nativeEvent,
) {
switch (domEventName) {
case 'focusin':
case 'focusout':
// 緩存 focusin /focusout 焦點過渡事件
queuedFocus = null;
break;
case 'dragenter':
case 'dragleave':
// 緩存 dragenter /dragleave 拖拽進出事件
queuedDrag = null;
break;
case 'mouseover':
case 'mouseout':
// 緩存 mouseover /mouseout 鼠標懸浮進出事件
queuedMouse = null;
break;
case 'pointerover':
case 'pointerout': {
// 緩存 pointerover/pointerout 指針懸浮事件
const pointerId = nativeEvent.pointerId;
queuedPointers.delete(pointerId);
break;
}
case 'gotpointercapture':
case 'lostpointercapture': {
// 緩存 gotpointercapture/lostpointercapture 指針捕獲事件
const pointerId = nativeEvent.pointerId;
queuedPointerCaptures.delete(pointerId);
break;
}
}
}
緩存高頻過渡類事件:queueIfContinuousEvent
focusin / dragenter / mouseover / pointerover / gotpointercapture 均為高頻、成對切換、瞬時狀態(tài)過渡事件,短時間會大量連續(xù)觸發(fā);SSR 半水合階段如果立刻執(zhí)行會阻塞主線程,且大量中間事件無業(yè)務價值,只需要保留最新一次回放
- 匹配到可緩存的連續(xù)過渡事件:存入緩存并返回 true;上層執(zhí)行 nativeEvent.stopPropagation(),終止本次事件分發(fā)
- 不匹配任何 case(click/input/scroll/wheel 等):返回 false,上層繼續(xù)走正常阻塞事件分發(fā)邏輯
export function queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
switch (domEventName) {
case 'focusin': {
const focusEvent = nativeEvent;
queuedFocus = accumulateOrCreateContinuousQueuedReplayableEvent(
queuedFocus,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
focusEvent,
);
return true;
}
case 'dragenter': {
const dragEvent = nativeEvent;
queuedDrag = accumulateOrCreateContinuousQueuedReplayableEvent(
queuedDrag,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
dragEvent,
);
return true;
}
case 'mouseover': {
const mouseEvent = nativeEvent;
queuedMouse = accumulateOrCreateContinuousQueuedReplayableEvent(
queuedMouse,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
mouseEvent,
);
return true;
}
case 'pointerover': {
const pointerEvent = nativeEvent;
const pointerId = pointerEvent.pointerId;
queuedPointers.set(
pointerId,
accumulateOrCreateContinuousQueuedReplayableEvent(
queuedPointers.get(pointerId) || null,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
pointerEvent,
),
);
return true;
}
case 'gotpointercapture': {
const pointerEvent = nativeEvent;
const pointerId = pointerEvent.pointerId;
queuedPointerCaptures.set(
pointerId,
accumulateOrCreateContinuousQueuedReplayableEvent(
queuedPointerCaptures.get(pointerId) || null,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
pointerEvent,
),
);
return true;
}
}
return false;
}
連續(xù)過渡事件緩存工具:accumulateOrCreateContinuousQueuedReplayableEvent
- 無舊緩存 / 原生事件對象不同:調(diào)用 createQueuedReplayableEvent 新建一份可回放事件快照,統(tǒng)一封裝回放所需全部上下文,后續(xù)水合完成后遍歷緩存、執(zhí)行事件分發(fā)時,僅依賴 QueuedReplayableEvent 即可完整復現(xiàn)當時事件環(huán)境,無需額外保存臨時變量,并對阻塞 Fiber 提升水合優(yōu)先級,調(diào)度器會優(yōu)先執(zhí)行這個組件的水合邏輯,盡快解除阻塞,減少事件持續(xù)緩存積壓,避免大量過渡事件長期駐留內(nèi)存
- 保存阻塞節(jié)點、事件名稱、初始 flags
- targetContainers 數(shù)組存入當前根容器
- 綁定原生事件對象作為唯一標識
- 舊緩存會被本次新快照覆蓋,舊快照失去引用,等待 GC 回收,只保留最新一次交互狀態(tài)
- 從阻塞 DOM 取出對應 Fiber
- 標記該 Fiber 為高優(yōu)連續(xù)水合任務
- 返回新建快照,覆蓋全局緩存
- 已經(jīng)存在同一份原生事件緩存:合并標記、收集多根容器,復用舊快照(高頻過渡事件短時間大量觸發(fā),如果每次都新建快照會頻繁 GC),不重復創(chuàng)建,減少內(nèi)存對象創(chuàng)建,降低 GC 開銷
- 把本次監(jiān)聽的捕獲 / 非委托標記合并到舊快照,回放時同時攜帶兩套標記
- 快照內(nèi) targetContainers 是數(shù)組,存儲所有觸發(fā)該事件的根容器;如果當前根容器不在數(shù)組中,追加進去;保證回放時能在所有關聯(lián) root 分發(fā)事件
- 返回原有合并后的快照
function accumulateOrCreateContinuousQueuedReplayableEvent(
existingQueuedEvent,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
if (
existingQueuedEvent === null ||
existingQueuedEvent.nativeEvent !== nativeEvent
) {
const queuedEvent = createQueuedReplayableEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
// 給被阻塞的 Fiber 打上連續(xù)事件水合優(yōu)先級,調(diào)度器優(yōu)先處理該組件水合,盡快解除阻塞
attemptContinuousHydration(fiber);
}
}
return queuedEvent;
}
existingQueuedEvent.eventSystemFlags |= eventSystemFlags;
const targetContainers = existingQueuedEvent.targetContainers;
if (
targetContainer !== null &&
targetContainers.indexOf(targetContainer) === -1
) {
targetContainers.push(targetContainer);
}
return existingQueuedEvent;
}
function createQueuedReplayableEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
return {
blockedOn,
domEventName,
eventSystemFlags,
nativeEvent,
targetContainers: [targetContainer],
};
}
事件邊界校正器 + 批量更新入口:dispatchEventForPluginEventSystem
- 對常規(guī)委托事件,修正事件向上遍歷的頂層邊界 ancestorInst,保證事件冒泡僅在當前 React 根容器內(nèi)部傳播,不跨 root、不跨 Portal 污染其他應用
- 單根應用普通 DOM:向上遍歷 Fiber 命中當前 HostRoot,break 循環(huán),ancestorInst 停在根 Fiber,事件冒泡范圍整個 root 內(nèi)部
- Portal 內(nèi)部觸發(fā)事件
- Portal 容器不屬于當前 root,內(nèi)層循環(huán)向上查找父 Fiber
- 若 Portal 掛載到外部 DOM,直接 return,事件不會冒泡到 Portal 外部組件
- 若 Portal 容器 DOM 能找到內(nèi)部托管組件,重置起點重新查找根邊界
- 多 Root 并行應用:每個 root 獨立綁定事件,邊界校正會限制冒泡僅在當前 root 內(nèi),A 根事件不會觸發(fā) B 根組件回調(diào),隔離多應用
- 把插件事件執(zhí)行邏輯包裹進批量更新,統(tǒng)一觸發(fā)批量渲染
export function dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
) {
let ancestorInst = targetInst;
if (
// 當前 DOM 是 React 托管組件,不是第三方外部 DOM
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
// 當前事件支持冒泡委托(click/input 等常規(guī)事件,不是 scroll/focus 這類不冒泡事件)
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = targetContainer;
if (targetInst !== null) {
let node = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
// 判斷 DOM 容器是否為當前事件綁定的根容器
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
let grandNode = node.return;
while (grandNode !== null) {
const grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
const grandContainer = grandNode.stateNode.containerInfo;
if (
isMatchingRootContainer(grandContainer, targetContainerNode)
) {
return;
}
}
grandNode = grandNode.return;
}
}
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (
parentTag === HostComponent ||
parentTag === HostText ||
parentTag === HostHoistable ||
parentTag === HostSingleton
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
// 批量更新,內(nèi)部回調(diào)所有 setState 會合并批量渲染,減少重渲染
batchedUpdates(() =>
// 事件插件體系真正執(zhí)行邏輯,生成合成事件、遍歷 Fiber 執(zhí)行 onXxx 回調(diào)
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
作用
- 初始化事件向上冒泡遍歷 Fiber 樹的終止邊界節(jié)點 ancestorInst
- 僅對托管 + 可委托事件做邊界裁剪:當前 DOM 是 React 托管組件,不是第三方外部 DOM 且當前事件支持冒泡委托(click/input 等常規(guī)事件,不是 scroll/focus 這類不冒泡事件)
- 僅目標存在 Fiber 才執(zhí)行邊界遍歷
- 向上遍歷 Fiber 樹,尋找當前根容器邊界:從當前 Fiber 持續(xù)向上 node.return 遍歷父 Fiber
- 遍歷到 HostRoot / HostPortal(根 / 傳送門)
- 取出 Root/Portal 對應的真實 DOM 容器
- 如果容器等于當前事件綁定的根容器 → 命中邊界,跳出循環(huán),當前 node 作為冒泡終點
- 不匹配則進入 Portal 特殊處理邏輯
- Portal 特殊處理:跨 Portal 不能冒泡到外部根,Portal 節(jié)點渲染到外部 DOM,邏輯上父級不在當前 root 內(nèi)部,隔離傳送門事件作用域
- 向上查找 Portal 的父 Fiber 鏈
- 若找到當前根容器,說明 Portal 渲染范圍在外部,直接 return 終止整個函數(shù)
- 向上回溯 DOM 容器,重新定位容器內(nèi) Fiber(Portal 跨 DOM 場景修復):Portal 渲染到外部 DOM,F(xiàn)iber 樹和 DOM 樹層級不一致
- 從 Portal 容器 DOM 向上遍歷原生 DOM
- 找到第一個 React 托管 DOM 組件 Fiber
- 重置遍歷起點 node 和冒泡邊界 ancestorInst,重啟主循環(huán)重新查找根邊界
- 找不到托管 Fiber 直接 return,終止事件分發(fā)
- 未命中 Root/Portal,繼續(xù)向上遍歷父 Fiber
- 邊界修正完成,進入批量執(zhí)行完整合成事件流程
設計意義
- 多根、Portal 事件隔離,修復原生 DOM 冒泡穿透問題:瀏覽器原生 DOM 冒泡不受 React 根容器、Portal 邊界限制,會跨 DOM 樹傳播;本函數(shù)通過校正 ancestorInst 限定事件遍歷上限,實現(xiàn)多 #root 應用事件互相隔離和 Portal 內(nèi)部事件不冒泡到外部父組件。處理 Portal 渲染到外部 DOM 導致 Fiber、DOM 層級不匹配的問題,抹平 DOM 樹與 Fiber 樹層級不一致的行為差異
- 統(tǒng)一批量更新包裝:所有用戶事件回調(diào)統(tǒng)一包裹在 batchedUpdates,自動合并多次狀態(tài)更新,避免高頻事件重復渲染,保障頁面性能
事件收集與回調(diào)執(zhí)行:dispatchEventsForPlugins
- 調(diào)用插件系統(tǒng),從 Fiber 樹提取所有捕獲 / 冒泡事件處理器,存入調(diào)度隊列
- 按階段順序執(zhí)行隊列里的所有事件回調(diào)
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
) {
const nativeEventTarget = getEventTarget(nativeEvent);
// 事件調(diào)度隊列,數(shù)組結(jié)構(gòu),存儲每條事件處理記錄(包含階段、回調(diào)、Fiber 實例、合成事件配置等)
const dispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// 按捕獲先、冒泡后的順序遍歷隊列,創(chuàng)建 SyntheticEvent 合成事件、執(zhí)行用戶回調(diào)、處理事件池、阻止冒泡 / 默認行為
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
作用
- 調(diào)用 getEventTarget 獲取標準化原生事件目標 DOM
- 初始化空事件調(diào)度隊列 dispatchQueue:存放所有待執(zhí)行的事件處理器。每條隊列項會記錄事件階段(捕獲 / 冒泡)、對應 Fiber、事件回調(diào)、插件配置等信息
- 調(diào)用 extractEvents 插件體系提取所有事件處理器,填充隊列,執(zhí)行完畢后 dispatchQueue 已有序存放全部待執(zhí)行回調(diào)
- 遍歷 React 內(nèi)置所有事件插件(點擊插件、表單插件、焦點插件、觸摸插件等)
- 從 targetInst 向上遍歷 Fiber 樹,直到上層限定的邊界 ancestorInst
- 區(qū)分捕獲階段回調(diào) (onXxxCapture) 、冒泡階段回調(diào) (onXxx) ,分別推入隊列
- 插件負責映射原生事件與 React 合成事件、處理特殊事件(如 onChange、onSelect)
- 調(diào)用 processDispatchQueue 按階段順序執(zhí)行隊列內(nèi)所有事件
- 區(qū)分捕獲 / 冒泡順序:先執(zhí)行全部捕獲回調(diào),再執(zhí)行全部冒泡回調(diào),模擬 DOM 原生事件傳播模型
- 復用事件池,創(chuàng)建 SyntheticEvent 合成事件對象,掛載原生事件、stopPropagation/preventDefault 封裝方法
- 依次執(zhí)行用戶寫的事件回調(diào)函數(shù)
- 監(jiān)聽回調(diào)中調(diào)用 e.stopPropagation(),截斷后續(xù)同階段事件執(zhí)行
- 回調(diào)執(zhí)行完畢回收合成事件對象,放回事件池復用,減少 GC 開銷
設計思想
- 優(yōu)先級包裝器策略:createEventListenerWrapperWithPriority 根據(jù)事件類型選擇 dispatchDiscreteEvent/dispatchContinuousEvent/dispatchEvent,在原生事件觸發(fā)時同步設置 currentUpdatePriority,決定后續(xù) setState 的 lane
- 被動事件仿 document 行為:Chrome 對 document 上的 touchstart/touchmove/wheel 強制 { passive: true },React 19 在 rootContainer 上手動模擬(非被動時瀏覽器阻塞滾動等待 JS)
- 偏函數(shù)閉包緩存:bind(null, domEventName, eventSystemFlags, targetContainer) 創(chuàng)建一次性閉包,原生 addEventListener 的 listener 引用在整個 root 生命周期中不變,無需重新綁定
到此這篇關于React 事件系統(tǒng)addTrappedEventListener的使用的文章就介紹到這了,更多相關React addTrappedEventListener內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解開發(fā)react應用最好用的腳手架 create-react-app
本篇文章主要介紹了詳解開發(fā)react應用最好用的腳手架 create-react-app,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04
React使用xlsx和js-export-excel實現(xiàn)前端導出
這篇文章主要為大家詳細介紹了React如何分別使用xlsx和js-export-excel實現(xiàn)前端導出功能,文中的示例代碼講解詳細,感興趣的小伙伴可以了解下2024-02-02
react antd-mobile ActionSheet+tag實現(xiàn)多選方式
這篇文章主要介紹了react antd-mobile ActionSheet+tag實現(xiàn)多選方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
React Native自定義Android的SSL證書鏈校驗
這篇文章主要為大家介紹了React Native自定義Android的SSL證書鏈校驗示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
關于react ant 組件 Select下拉框 值回顯的問題
這篇文章主要介紹了關于react ant 組件 Select下拉框 值回顯的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08

