Appearance
第14章 合成事件系统
本章要点
- 事件委托的演进:从 React 16 的 document 委托到 React 17+ 的 root 容器委托
- SyntheticEvent 的设计哲学:跨浏览器一致性与性能的平衡
- 事件插件系统(Event Plugin System)的架构与注册机制
- 事件优先级模型:Discrete、Continuous、Default 三级优先级与 Lane 的映射
listenToAllSupportedEvents:React 如何在挂载时一次性注册所有事件监听dispatchEvent的完整链路:从原生事件到 React 回调的调度过程- React 19 中事件系统的简化:移除事件池化与遗留兼容逻辑
如果你在 React 组件上写过 onClick、onChange、onScroll,你可能从未思考过一个问题:这些事件处理器实际上并没有绑定在你期望的那个 DOM 节点上。
这不是一个 bug,而是 React 最精妙的设计之一——合成事件系统(Synthetic Event System)。React 没有在每个 <button> 上调用 addEventListener,而是在应用的根容器上统一监听所有事件,然后通过 Fiber 树自己完成事件的分发和冒泡。这个设计的影响是深远的:它让 React 可以控制事件的优先级、批量更新的时机,甚至可以在不同的渲染器(DOM、Native、Canvas)之间共享同一套事件逻辑。
要理解 React 的事件系统,你需要暂时忘记 DOM 事件模型——捕获、目标、冒泡这三个阶段仍然存在,但它们的实现方式被 React 彻底重写了。从 React 17 开始,事件委托的目标从 document 迁移到了 root 容器;从 React 18 开始,事件的触发与调度器深度耦合;到了 React 19,事件系统又经历了一轮显著的精简。让我们从头追溯这个演进过程。
14.1 事件委托:从 document 到 root 的演进
14.1.1 传统 DOM 事件绑定的问题
在没有框架的世界里,给 1000 个列表项绑定点击事件意味着调用 1000 次 addEventListener。每一个监听器都会消耗内存,每一次绑定和解绑都有性能成本。事件委托(Event Delegation)是解决这个问题的经典模式:
typescript
// 传统事件委托
const list = document.getElementById('list');
list.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
if (target.tagName === 'LI') {
handleItemClick(target.dataset.id);
}
});React 从诞生之日起就内建了事件委托,但它把委托做到了极致——不是委托到父容器,而是委托到整个应用的顶层。
14.1.2 React 16 及之前:委托到 document
在 React 16 及之前的版本中,所有事件监听器都被注册在 document 上:
typescript
// React 16 的事件注册(简化)
// packages/react-dom/src/events/ReactBrowserEventEmitter.js
function listenTo(
registrationName: string, // 如 'onClick'
mountAt: Document | Element // 始终是 document
) {
const listeningSet = getListeningSetForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (const dependency of dependencies) {
if (!listeningSet.has(dependency)) {
// 在 document 上注册原生事件监听
trapEventForPluginEventSystem(dependency, mountAt);
listeningSet.add(dependency);
}
}
}这个设计在大多数场景下工作良好,但存在一个致命的问题:多个 React 应用实例的事件会互相干扰。
tsx
// 微前端场景:两个 React 应用共存
// App A (React 16)
ReactDOM.render(<AppA />, document.getElementById('app-a'));
// App B (React 16)
ReactDOM.render(<AppB />, document.getElementById('app-b'));
// 问题:两个应用的事件都委托到了 document
// App A 中调用 e.stopPropagation() 会阻止 App B 的事件14.1.3 React 17+:委托到 root 容器
React 17 做出了一个看似简单但影响深远的改变——将事件委托的目标从 document 改为 root 容器:
typescript
// React 17+ 的事件注册
// packages/react-dom/src/events/DOMPluginEventSystem.js
function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement as any)[listeningMarker]) {
(rootContainerElement as any)[listeningMarker] = true;
allNativeEvents.forEach((domEventName) => {
// 大部分事件同时注册捕获和冒泡阶段
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false, // 冒泡阶段
rootContainerElement
);
}
listenToNativeEvent(
domEventName,
true, // 捕获阶段
rootContainerElement
);
});
}
}注意 allNativeEvents 这个集合。它包含了 React 支持的所有原生事件名——click、keydown、scroll、pointerdown 等等。React 在应用挂载的那一刻,就在 root 容器上注册了所有事件的监听器,而不是按需注册。这是一个以空间换时间的设计决策:
typescript
// packages/react-dom/src/events/EventRegistry.js
export const allNativeEvents: Set<DOMEventName> = new Set();
// 事件插件在初始化时注册它们关心的原生事件
export function registerTwoPhaseEvent(
registrationName: string, // 如 'onClick'
dependencies: Array<DOMEventName> // 如 ['click']
) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>
) {
// 将依赖的原生事件加入全局集合
for (const dependency of dependencies) {
allNativeEvents.add(dependency);
}
}深度洞察:为什么 React 选择一次性注册所有事件,而不是在组件首次使用
onClick时才注册click监听?原因是确定性(Determinism)。如果事件监听是惰性的,那么同一个原生事件在不同时机可能有不同的行为——取决于是否已有组件注册了对应的 React 事件。一次性注册消除了这种时序依赖,让事件系统的行为完全可预测。
14.1.4 listenToNativeEvent 的实现
listenToNativeEvent 是实际调用 addEventListener 的地方:
typescript
// packages/react-dom/src/events/DOMPluginEventSystem.js
function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget
) {
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener
);
}
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean
) {
// 根据事件类型创建不同优先级的监听器
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);
let unsubscribeListener;
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
}
}
// 最终落到原生 API
function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}