前言
React 18.3.1 使用合成事件的核心原因
1. 统一浏览器行为
不同浏览器(如 IE、Chrome、Firefox)对原生事件(如 focus、scroll)的实现差异较大,合成事件通过标准化 API 屏蔽底层差异,确保跨浏览器一致性。
2. 优化事件系统架构
React 需要将事件逻辑与组件生命周期、虚拟 DOM 更新深度绑定,合成事件为 React 提供了可控制的事件流机制。
合成事件的核心优势
1. 跨浏览器一致性
- 标准化 API: 无论用户使用何种浏览器,合成事件(如 onClick、onChange)的行为和属性完全一致。
- 修复浏览器差异: 例如,onScroll 在原生事件中不冒泡,但 React 通过合成事件模拟冒泡逻辑,使其行为更符合开发者直觉。
2. 性能优化
- 事件委托(Event Delegation): React 17+ 将事件统一绑定到应用根容器(如 #root),而非每个 DOM 节点,大幅减少内存占用和绑定开销。
- 自动清理监听器: 组件卸载时,React 自动解除相关事件监听,避免内存泄漏(无需手动 removeEventListener)。
3. 对 React 生态的深度集成
- 与组件生命周期同步: 合成事件的处理逻辑会考虑组件状态(如异步更新的 setState),避免因事件触发时组件已卸载导致的错误。
- 支持批量更新: 在合成事件回调中的状态更新会自动合并为批量更新,减少不必要的渲染。
4. 未来兼容性与扩展性
- 渐进式升级: 合成事件系统使 React 能独立于浏览器演进,例如 React 17 将事件委托目标从 document 改为根容器,解决微前端场景下的多应用冲突。
- 自定义事件支持: 开发者可通过合成事件系统扩展自定义事件(如长按、手势),无需依赖第三方库。
5. 开发体验提升
- 异步访问事件属性: React 17+ 移除了事件池(Event Pooling),开发者无需调用 e.persist() 即可在异步代码中直接使用事件对象。
- 更直观的冒泡行为: 修正了原生事件中 onFocus/onBlur 不冒泡的问题,改用 onFocusIn/onFocusOut 模拟冒泡,逻辑更统一。
整体流程图
svg图片,可以在新标签页中打开图片查看大图
合成事件注册listenToAllSupportedEvents
ReactDOM.createRoot()
一系列对象的初始化和事件初始化
function createRoot(container, options) {
//...
var root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError
);
markContainerAsRoot(root.current, container);
//...
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
从上面的初始化代码可以知道,我们会触发listenToAllSupportedEvents()
进行事件的注册
对于非 selectionchange
事件,会判断是否是委托事件,如果是非委托事件,则触发
listenToNativeEvent(domEventName, false, rootContainerElement)
listenToNativeEvent(domEventName, true, rootContainerElement)
否则就只触发
listenToNativeEvent(domEventName, true, rootContainerElement)
对于selectionchange
事件,触发
listenToNativeEvent("selectionchange", false, ownerDocument)
绑定到ownerDocument
上,而不是上面的rootContainerElement
最终触发的还是 listenToNativeEvent()
方法
function listenToAllSupportedEvents(rootContainerElement) {
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach(function (domEventName) {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== "selectionchange") {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
var ownerDocument =
rootContainerElement.nodeType === DOCUMENT_NODE
? rootContainerElement
: rootContainerElement.ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!ownerDocument[listeningMarker]) {
ownerDocument[listeningMarker] = true;
listenToNativeEvent("selectionchange", false, ownerDocument);
}
}
}
}
而在 listenToNativeEvent()
方法中,其中触发的是 addTrappedEventListener()
而addTrappedEventListener()
主要做两件事:
createEventListenerWrapperWithPriority()
: 合成事件的监听事件的构建listener
- 触发原生API
target.addEventListener(eventType, listener, {...})
进行targetContainer
与各种原生事件
的绑定
addEventCaptureListenerWithPassiveFlag
、addEventCaptureListener
、addEventBubbleListenerWithPassiveFlag
、addEventBubbleListener
本质都是target.addEventListener()
,只是参数不同而已
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
var eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
function addTrappedEventListener() {
var listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);
var isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
if (
domEventName === "touchstart" ||
domEventName === "touchmove" ||
domEventName === "wheel"
) {
isPassiveListener = true;
}
}
targetContainer = targetContainer;
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
addEventCaptureListener(targetContainer, domEventName, listener);
}
} else {
if (isPassiveListener !== undefined) {
addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
addEventBubbleListener(targetContainer, domEventName, listener);
}
}
}
在 createEventListenerWrapperWithPriority()
中,会根据事件名称去返回不同的方法:
dispatchDiscreteEvent
dispatchContinuousEvent
dispatchEvent
这里的返回的 listenerWrapper.bind()
在上面的分析中,将传入addEventCaptureListenerWithPassiveFlag
、addEventCaptureListener
、addEventBubbleListenerWithPassiveFlag
、addEventBubbleListener
进行原生事件的绑定
本质都是调用
dispatchEvent
,下面的小节将展开详细分析
function createEventListenerWrapperWithPriority() {
var eventPriority = getEventPriority(domEventName);
var 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);
}
而从下面的代码可以知道,dispatchDiscreteEvent
和 dispatchContinuousEvent
无非就是设置下 setCurrentUpdatePriority()
当前的update优先级,然后触发dispatchEvent()
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
var previousPriority = getCurrentUpdatePriority();
var prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
function dispatchContinuousEvent(domEventName, eventSystemFlags, container, nativeEvent) {
var previousPriority = getCurrentUpdatePriority();
var prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
try {
setCurrentUpdatePriority(ContinuousEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
因此我们的重点将放在
dispatchEvent()
的分析上
dispatchEvent
本质触发dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay()
function dispatchEvent() {
if (!_enabled) {
return;
}
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
);
}
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay()
先触发findInstanceBlockingEvent()
检测当前事件是否会被堵塞
如果没有被堵塞,则触发dispatchEventForPluginEventSystem()
将事件分发到各个插件系统,然后触发clearIfContinuousEvent()
终止未完成的连续事件处理任务
function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
) {
var blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
);
if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer
);
clearIfContinuousEvent(domEventName, nativeEvent);
return;
}
//...处理SSR和SuspenseComponent相关逻辑
}
findInstanceBlockingEvent()
从下面代码可以看出,主要还是
getEventTarget()
: 通过原生事件找到对应的DOM元素getClosestInstanceFromNode()
: 通过原生DOM拿到fiber,如果获取不到,则不断向上寻找最近一个fibergetNearestMountedFiber()
: 当前fiber是不是可能已经不再渲染了?通过getNearestMountedFiber()
检测当前fiber是否已经还在Fiber Tree
上面,否则继续向上寻找fiber
最终将找到的fiber赋值给 return_targetInst
,然后返回 null
那么
getEventTarget()
、getClosestInstanceFromNode()
、getNearestMountedFiber()
到底是如何执行的呢?我们接下来将对这几个方法展开具体分析
function findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
) {
return_targetInst = null;
var nativeEventTarget = getEventTarget(nativeEvent);
var targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (targetInst !== null) {
var nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
var tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
//...
} else if (tag === HostRoot) {
var root = nearestMounted.stateNode;
if (isRootDehydrated(root)) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
targetInst = null;
}
}
}
return_targetInst = targetInst;
return null;
}
getEventTarget
根据原生事件去获取原生DOM数据
function getEventTarget(nativeEvent) {
// Fallback to nativeEvent.srcElement for IE9
// https://github.com/facebook/react/issues/12506
var target = nativeEvent.target || nativeEvent.srcElement || window; // Normalize SVG <use> element events #4963
if (target.correspondingUseElement) {
target = target.correspondingUseElement;
} // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
// @see http://www.quirksmode.org/js/events_properties.html
return target.nodeType === TEXT_NODE ? target.parentNode : target;
}
getClosestInstanceFromNode
使用DOM
获取对应的 fiber,而targetNode[internalInstanceKey]
是在 completeWork()
的HostComponent
根据fiber去构建DOM时进行映射
如果能够正确拿到对应的 fiber,直接返回 targetInst
如果不存在对应的 fiber,则需要不断向上 targetNode.parentNode
寻找,直到找到最近的DOM对应的fiber
function getClosestInstanceFromNode(targetNode) {
var targetInst = targetNode[internalInstanceKey];
if (targetInst) {
return targetInst;
}
var parentNode = targetNode.parentNode;
while (parentNode) {
targetInst =
parentNode[internalContainerInstanceKey] ||
parentNode[internalInstanceKey];
if (targetInst) {
var alternate = targetInst.alternate;
if (
targetInst.child !== null ||
(alternate !== null && alternate.child !== null)
) {
var suspenseInstance = getParentSuspenseInstance(targetNode);
while (suspenseInstance !== null) {
//...
}
}
return targetInst;
}
targetNode = parentNode;
parentNode = targetNode.parentNode;
}
return null;
}
getNearestMountedFiber
传入对应的 fiber,判断当前 fiber.alternate 是否存在,如果不存在说明该 fiber 已经被卸载,那么就需要重新 node.return
寻找下一个不具备 Placement
的 fiber数据
确定完成 fiber 后,使用 node.return
找到最顶端的 fiber,检测是否是 HostRoot
,如果是的话,返回上面找到的 fiber 数据;如果不是,则返回 null
function getNearestMountedFiber(fiber) {
var node = fiber;
var nearestMounted = fiber;
if (!fiber.alternate) {
var nextNode = node;
do {
node = nextNode;
if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
nearestMounted = node.return;
}
nextNode = node.return;
} while (nextNode);
} else {
while (node.return) {
node = node.return;
}
}
if (node.tag === HostRoot) {
return nearestMounted;
}
return null;
}
dispatchEventForPluginEventSystem事件派发到插件系统
检测当前 fiber (不断fiber=fiber.return)找到的HostRoot对应的DOM
跟目前传入的targetContainerNode
是否不同,不同则需要重新寻找
注:targetContainerNode
是createContainer()
时传入的#root DOM
function dispatchEventForPluginEventSystem() {
var ancestorInst = targetInst;
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
var targetContainerNode = targetContainer;
if (targetInst !== null) {
var node = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
var nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
var container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
//...
}
while (container !== null) {
var parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
var parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
batchedUpdates(function () {
return dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst
);
});
}
dispatchEventsForPlugins
中,根据事件拿到对应的 DOM 元素,然后触发 -extractEvents()
将对应的回调函数加入到dispatchQueue
数组中 -processDispatchQueue()
处理 dispatchQueue
数组,不断遍历执行对应的回调函数 listener
function dispatchEventsForPlugins() {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents()
在这个方法中,会触发不同类型事件的extractEvents()
事件
function extractEvents() {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
//...
}
}
SimpleEventPlugin.extractEvents:
React 一开始就会自动执行 registerSimpleEvents()
方法,进行 多个原生事件
-> React事件名称
的绑定,存放在 topLevelEventsToReactNames
中
function registerSimpleEvents() {
for (var i = 0; i < simpleEventPluginEvents.length; i++) {
var eventName = simpleEventPluginEvents[i];
var domEventName = eventName.toLowerCase();
var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, "on" + capitalizedEvent);
}
registerSimpleEvent(ANIMATION_END, "onAnimationEnd");
registerSimpleEvent(ANIMATION_ITERATION, "onAnimationIteration");
registerSimpleEvent(ANIMATION_START, "onAnimationStart");
registerSimpleEvent("dblclick", "onDoubleClick");
registerSimpleEvent("focusin", "onFocus");
registerSimpleEvent("focusout", "onBlur");
registerSimpleEvent(TRANSITION_END, "onTransitionEnd");
}
var topLevelEventsToReactNames = new Map(); // NOTE: Capitalization is important in this list!
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
执行 SimpleEventPlugin.extractEvents()
时
- 先从
topLevelEventsToReactNames
中利用原生的DOM事件名称
获取React事件名称
- 触发
accumulateSinglePhaseListeners()
传入fiber
+React事件名称
(比如onClick
),通过fiber.props
上的属性,获取对应的监听方法 - 创建合成事件对象
_event = new SyntheticEventCtor()
- 将合成事件对象 +
fiber.props
获取到的回调函数数组加入到dispatchQueue
中
// SimpleEventPlugin.extractEvents
function extractEvents$4() {
var reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
var SyntheticEventCtor = SyntheticEvent;
var reactEventType = domEventName;
switch (domEventName) {
case "keydown":
case "keyup":
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case "focusin":
reactEventType = "focus";
SyntheticEventCtor = SyntheticFocusEvent;
break;
//...
}
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
var accumulateTargetOnly = !inCapturePhase && domEventName === "scroll";
var _listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly
);
if (_listeners.length > 0) {
var _event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);
dispatchQueue.push({
event: _event,
listeners: _listeners,
});
}
}
accumulateSinglePhaseListeners()
根据传入的reactName
(比如 onClick
),使用getListener()
从props
上获取对应的回调方法
然后从当前传入的 fiber 向上遍历 instance = instance.return
,如果都是HostComponent
都创建对应的回调方法
比如冒泡阶段,当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序
因此从当前fiber => 当前fiber的祖先 都应该触发对应的方法,比如点击某一个 button 的 onClick 的事件,那么 当前fiber + 当前fiber的父亲...当前fiber的祖先 都触发 onClick 事件
function accumulateSinglePhaseListeners(reactName, ...) {
var captureName = reactName !== null ? reactName + "Capture" : null;
var reactEventName = inCapturePhase ? captureName : reactName;
var listeners = [];
var instance = targetFiber;
var lastHostComponent = null;
while (instance !== null) {
var _instance2 = instance,
stateNode = _instance2.stateNode,
tag = _instance2.tag;
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
if (reactEventName !== null) {
var listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent)
);
}
}
}
if (accumulateTargetOnly) {
break;
}
instance = instance.return;
}
return listeners;
}
function getListener(inst, registrationName) {
var stateNode = inst.stateNode;
if (stateNode === null) {
return null;
}
var props = getFiberCurrentPropsFromNode(stateNode);
if (props === null) {
return null;
}
var listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
return listener;
}
function createDispatchListener(instance, listener, currentTarget) {
return {
instance: instance,
listener: listener,
currentTarget: currentTarget,
};
}
processDispatchQueue()
拿出dispatchQueue
队列中的数据
- 如果是
inCapturePhase
捕获阶段,则从dispatchListeners.length - 1
开始遍历 - 如果是冒泡阶段,则从
0
开始遍历
冒泡: 当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序
捕获:
- 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
- 目标阶段(Target phase)—— 事件到达目标元素。
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (var i = 0; i < dispatchQueue.length; i++) {
var _dispatchQueue$i = dispatchQueue[i],
event = _dispatchQueue$i.event,
listeners = _dispatchQueue$i.listeners;
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
}
function processDispatchQueueItemsInOrder() {
var previousInstance;
if (inCapturePhase) {
for (var i = dispatchListeners.length - 1; i >= 0; i--) {
var _dispatchListeners$i = dispatchListeners[i],
instance = _dispatchListeners$i.instance,
currentTarget = _dispatchListeners$i.currentTarget,
listener = _dispatchListeners$i.listener;
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (var _i = 0; _i < dispatchListeners.length; _i++) {
//...跟上面代码逻辑一致
}
}
}
executeDispatch():
核心代码就是执行传入的listener
方法
function executeDispatch(event, listener, currentTarget) {
var type = event.type || "unknown-event";
event.currentTarget = currentTarget;
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
function invokeGuardedCallbackAndCatchFirstError(name, func, ...) {
invokeGuardedCallback.apply(this, arguments);
}
var reporter = {
onError: function (error) {
hasError = true;
caughtError = error;
},
};
function invokeGuardedCallback(name, func, ...) {
invokeGuardedCallbackImpl.apply(reporter, arguments);
}
var invokeGuardedCallbackImpl = invokeGuardedCallbackProd;
function invokeGuardedCallbackProd(name, func, ...) {
var funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}
clearIfContinuousEvent
终止未完成的连续事件处理任务,确保高频事件场景下的性能优化和资源安全
function clearIfContinuousEvent(domEventName, nativeEvent) {
switch (domEventName) {
case "focusin":
case "focusout":
queuedFocus = null;
break;
case "dragenter":
case "dragleave":
queuedDrag = null;
break;
case "mouseover":
case "mouseout":
queuedMouse = null;
break;
case "pointerover":
case "pointerout": {
var pointerId = nativeEvent.pointerId;
queuedPointers.delete(pointerId);
break;
}
case "gotpointercapture":
case "lostpointercapture": {
var _pointerId = nativeEvent.pointerId;
queuedPointerCaptures.delete(_pointerId);
break;
}
}
}