Appearance
6.3 事件的分发和执行:监听器的收集与执行
在上一节中,我们看到 extractEvents 函数成功地将一个原生 DOM 事件转换成了一个或多个 React 合成事件,并将它们连同对应的监听器(listeners)一起推入了 dispatchQueue 队列。现在,是时候处理这个队列,真正地执行我们在组件中定义的事件处理函数了。
这个过程主要由 processDispatchQueue 和 processDispatchQueueItemsInOrder 这两个函数驱动,它们是 React 事件分发机制的核心。
监听器的收集:accumulateSinglePhaseListeners
在深入分发过程之前,我们必须先理解监听器是如何被收集的。这个任务由 accumulateSinglePhaseListeners 函数完成(该函数在 SimpleEventPlugin.js 内部被调用)。
它的核心职责是:从触发事件的 Fiber 节点开始,沿着 Fiber 树向上遍历直到根节点,收集所有路径上为当前事件(例如 onClick)定义的捕获和冒泡阶段的监听器。
javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
eventSystemFlags: EventSystemFlags,
): Array<DispatchListener> {
const bubbledName = reactName;
const capturedName = reactName + 'Capture';
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;
// 遍历从目标 Fiber 到根节点的路径
while (instance !== null) {
const {stateNode, tag} = instance;
// 只在 HostComponent(DOM 元素)上查找监听器
if (tag === HostComponent && stateNode !== null) {
const currentTarget = stateNode;
if (inCapturePhase) {
// 捕获阶段,查找 onClickCapture
const listener = getListener(instance, capturedName);
if (listener) {
listeners.push(createDispatchListener(instance, listener, currentTarget));
}
} else {
// 冒泡阶段,查找 onClick
const listener = getListener(instance, bubbledName);
if (listener) {
listeners.push(createDispatchListener(instance, listener, currentTarget));
}
}
}
instance = instance.return;
}
return listeners;
}设计决策与解析:
- 沿着 Fiber 树遍历:React 的组件层级关系体现在 Fiber 树上。通过
instance.return向上遍历,可以完美模拟 DOM 的事件冒泡/捕获路径。 - 区分捕获与冒泡:通过
inCapturePhase标志,函数可以准确地收集onClickCapture(捕获)或onClick(冒泡)的监听器。React 事件系统完整地在内部模拟了 W3C 的事件流模型。 - 性能优化:
getListener函数直接从 Fiber 节点的memoizedProps中获取事件处理器。这意味着事件监听器的查找速度非常快,因为它只是一个对象属性的访问,而无需查询真实的 DOM 元素。 DispatchListener对象:收集到的不是裸露的函数,而是一个DispatchListener对象,它封装了listener(处理器函数)、instance(Fiber 节点)和currentTarget(当前的 DOM 元素)。这为后续的事件执行提供了完整的上下文。
事件队列的处理:processDispatchQueue
当 extractEvents 完成它的工作后,dispatchEventsForPlugins 函数会立即调用 processDispatchQueue 来处理 dispatchQueue。
javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
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的存在使得 React 可以将由一个原生事件触发的多个 React 事件(例如,focusin同时触发onFocus和onFocusIn)收集起来,然后一次性按顺序处理。
按序执行监听器:processDispatchQueueItemsInOrder
这是事件执行的最后一站。它接收一个事件对象和一组监听器,并负责以正确的顺序调用它们。
javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
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;
}
}
}设计决策与解析:
模拟 DOM 事件流:
- 对于捕获阶段 (
inCapturePhase为true),循环从后往前遍历dispatchListeners数组。因为accumulateSinglePhaseListeners是从目标向根节点收集的,所以数组的末尾是离根节点最近的组件,最前面是离目标最近的组件。从后往前遍历正好实现了从父到子的捕获流。 - 对于冒泡阶段 (
inCapturePhase为false),循环从前往后遍历,实现了从子到父的冒泡流。
- 对于捕获阶段 (
支持
stopPropagation:在执行每个监听器之前,都会检查event.isPropagationStopped()。如果在任何一个监听器内部调用了event.stopPropagation(),这个函数会返回true,从而中断后续监听器的执行。这是对标准 DOM API 的忠实模拟。executeDispatch:这是一个简单的包装函数,它负责设置event.currentTarget(这对于事件处理非常重要),然后在一个try...catch块中安全地执行监听器,并捕获任何可能发生的错误,防止单个事件处理器的错误导致整个应用崩溃。
javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
// 在执行监听器前,正确设置 currentTarget
event.currentTarget = currentTarget;
try {
listener(event);
} catch (error) {
// 捕获错误并报告,而不是让整个应用崩溃
reportGlobalError(error);
}
// 执行完毕后,重置 currentTarget
event.currentTarget = null;
}总结
React 的事件分发和执行机制是一个精心设计的系统,它通过在内部模拟 DOM 的事件流,实现了高性能、跨浏览器一致且功能强大的事件处理能力。
整个流程可以总结为:
- 收集:
accumulateSinglePhaseListeners沿 Fiber 树向上遍历,收集捕获和冒泡的监听器。 - 入队:
extractEvents将合成事件和收集到的监听器打包,推入dispatchQueue。 - 处理:
processDispatchQueue遍历队列,对每个事件-监听器组合进行处理。 - 执行:
processDispatchQueueItemsInOrder根据捕获或冒泡阶段,按正确顺序遍历监听器数组,并通过executeDispatch安全地执行每一个事件处理器。
这个流程清晰地将“事件提取”、“监听器收集”和“事件执行”三个阶段解耦,每个部分都只做一件事,并把它做好。正是这种清晰的架构,使得 React 的事件系统既稳定又易于扩展。