「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」
react 版本:v17.0.3
合成事件思想
React 中有自己的事件系统模式,通常被称为 React合成事件。之所以采用这种自定义的合成事件,一方面是为了抹平浏览器差异性,使得 React 开发者不再需要去关注浏览器事件兼容性问题,另一方面是为了统一管理事件,提高性能,这主要体现在 React 内部实现事件委托,并且记录当前事件发生的状态上。
事件委托,也就是通常提到的事件代理机制,这种机制不会把事件处理函数直接绑定在真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听和处理函数。当组件加载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象,当事件发生时,首先被这个统一的事件监听器处理,延后在映射表里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大的提升。
记录当前事件发生的状态,即记录事件执行的上下文,这便于 React 处理不同事件的优先级,达到谁优先级高先处理谁的目的,这也就实现了 React 的增量渲染思想,可以预防掉帧,同时达到页面更顺滑的目的,提升用户体验。
以上便是 React 合成事件的思想。
事件委托的更改
在 React17 中,React 不再向 document 附加事件处理器。而是将事件处理器附加到渲染 React 树的根DOM容器中:
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
在 React16 或更早版本中,React 会对大多数事件执行 document.addEventListener()。React17 会在底层调用 rootNode.addEventListener()。
官网给出的图片清晰的展示了 v17.0.0 的改动,无论是在 document 还是 根DOM容器 上监听事件,都可以归为事件委托。
非事件委托
在 React 的事件体系中,并不是所有事件都是通过事件委托来实现的,有一些事件是直接绑定到目标DOM元素上的。这部分事件如下:
// packages/react-dom/src/events/DOMPluginEventSystem.js
// List of events that need to be individually attached to media elements.
// 媒体元素上的事件
export const mediaEventTypes: Array<DOMEventName> = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'encrypted',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'loadstart',
'pause',
'play',
'playing',
'progress',
'ratechange',
'resize',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting',
];
// We should not delegate these events to the container, but rather
// set them on the actual target element itself. This is primarily
// because these events do not consistently bubble in the DOM.
// 这些事件不会委托给根DOM容器,而是应该将它们绑定在实际的目标元素上,因为这些事件在DOM中不会冒泡
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
'cancel',
'close',
'invalid',
'load',
'scroll',
'toggle',
// In order to reduce bytes, we insert the above array of media events
// into this Set. Note: the "error" event isn't an exclusive media event,
// and can occur on other elements too. Rather than duplicate that event,
// we just take it from the media events array.
...mediaEventTypes,
]);
上述源码中的事件,在DOM中不会发生冒泡,因此React将它们直接绑定在了目标DOM元素上。
事件的绑定
在「React 源码解读之React应用的2种启动方式」一文中,介绍了 React 在启动的过程中会创建3个全局对象。其中 legacy模式在 legacyCreateRootFromDOMContainer 函数中创建 ReactDOMRoot 对象后调用 listenToAllSupportedEvents(rootContainerElement) 函数在根DOM容器(div#root) 上绑定事件。Concurrent模式则是在 createRoot 函数中创建 ReactDOMRoot 对象后调用listenToAllSupportedEvents(rootContainerElement) 函数在根DOM容器(div#root) 上绑定事件。
// 在根DOM容器(div#root)上监听事件
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
可见,两种启动模式都调用了listenToAllSupportedEvents来绑定事件。下面,我们来看看这个函数的实现。
listenToAllSupportedEvents
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// 1、节流优化, 保证全局注册只被调用一次
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
// 2、遍历 set集合 allNativeEvents,监听冒泡和捕获阶段的事件
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
// selectionchange 事件不会冒泡到document上,因此需要单独处理
// 将除了 selectionchange 之外的事件绑定到 根DOM容器上
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
// nonDelegatedEvents 存储着不会在DOM中冒泡的事件,如视频元素上的事件,scroll、load 事件等
// 这里将会冒泡到DOM上的事件绑定到根DOM容器上
// listenToNativeEvent 的第二个参入传入 false ,表示在冒泡阶段监听事件
listenToNativeEvent(domEventName, false, rootContainerElement);
}
// 这里将不会在DOM中冒泡的事件绑定在目标元素上
// listenToNativeEvent 的第二个参入传入 true ,表示在捕获阶段监听事件
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
// 3、绑定 selectionchange 事件
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
在 listenToAllSupportedEvents 函数中:
-
通过 Math.random 生成一个随机字符串 (listeningMarker),来保证 rootContainerElement 全局注册只被调用一次,从而实现节流优化的目的。
-
然后遍历 set集合allNativeEvents,根据事件的特点分别在捕获阶段和冒泡阶段注册事件监听。
-
nonDelegatedEvents变量存储着不会在DOM中冒泡的事件,如媒体元素(video标签等)上的事件,scroll、load等。这些事件在DOM中不会发生冒泡,React将它们直接注册在目标DOM元素上。
-
对于会在DOM中发生冒泡的事件,则将它们注册在根DOM容器(div#root)上。
// 2、遍历 set集合 allNativeEvents,监听冒泡和捕获阶段的事件 allNativeEvents.forEach(domEventName => { // We handle selectionchange separately because it // doesn't bubble and needs to be on the document. // selectionchange 事件不会冒泡到document上,因此需要单独处理 // 将除了 selectionchange 之外的事件绑定到 根DOM容器上 if (domEventName !== 'selectionchange') { if (!nonDelegatedEvents.has(domEventName)) {
// nonDelegatedEvents 存储着不会在DOM中冒泡的事件,如视频元素上的事件,scroll、load 事件等 // 这里将会冒泡到DOM上的事件绑定到根DOM容器上 // listenToNativeEvent 的第二个参入传入 false ,表示在冒泡阶段监听事件 listenToNativeEvent(domEventName, false, rootContainerElement); } // 这里将不会在DOM中冒泡的事件绑定在目标元素上 // listenToNativeEvent 的第二个参入传入 true ,表示在捕获阶段监听事件 listenToNativeEvent(domEventName, true, rootContainerElement);} });
-
最后是单独处理 selectionchange 事件,将其注册到目标DOM元素上。
// 3、绑定 selectionchange 事件 if (!(ownerDocument: any)[listeningMarker]) { (ownerDocument: any)[listeningMarker] = true; listenToNativeEvent('selectionchange', false, ownerDocument); }
无论是在捕获阶段还是在冒泡阶段注册事件监听,都是调用 listenToNativeEvent 函数,接下来看看这个函数。
listenToNativeEvent
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function listenToNativeEvent(
domEventName: DOMEventName, // 事件名称
isCapturePhaseListener: boolean, // 是在捕获阶段还是冒泡阶段监听事件
target: EventTarget, // 目标事件对象
): void {
// 删除了Dev部分的代码
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
// IS_CAPTURE_PHASE 标记事件是在捕获阶段
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// 注册事件监听
addTrappedEventListener(
target, // 目标事件对象
domEventName, // 事件名称
eventSystemFlags, //
isCapturePhaseListener, // boolean 值,false 表示在冒泡阶段监听事件,true 表示在捕获阶段监听事件
);
}
listenToNativeEvent 函数并不复杂,通过位运算给 eventSystemFlags 变量添加了一个 IS_CAPTURE_PHASE 的标记,标记事件是在捕获阶段。然后调用 addTrappedEventListener 函数来注册事件监听。
addTrappedEventListener
// packages/react-dom/src/events/DOMPluginEventSystem.js
function addTrappedEventListener(
targetContainer: EventTarget, // 目标事件对象
domEventName: DOMEventName, // 目标事件名称
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean, // false 表示在冒泡阶段监听事件,true 表示在捕获阶段监听事件
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 1、构造事件监听器
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
// If passive option is not supported, then the event will be
// active and not passive.
// 2、初始化被动事件监听器
let isPassiveListener = undefined;
// 某些浏览器(特别是Chrome和Firefox)已将文档级节点Window,Document和Document.body上的touchstart和touchmove事件的Passive选项的默认值更改为true
if (passiveBrowserEventsSupported) {
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true; // 设置为被动事件监听器
}
}
// 3、获取绑定事件的目标容器,是根DOM容器还是目标元素
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer;
let unsubscribeListener;
// When legacyFBSupport is enabled, it's for when we
// want to add a one time event listener to a container.
// This should only be used with enableLegacyFBSupport
// due to requirement to provide compatibility with
// internal FB www event tooling. This works by removing
// the event listener as soon as it is invoked. We could
// also attempt to use the {once: true} param on
// addEventListener, but that requires support and some
// browsers do not support this today, and given this is
// to support legacy code patterns, it's likely they'll
// need support for such browsers.
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
listener = function(...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener,
);
return originalListener.apply(this, p);
};
}
// TODO: There are too many combinations here. Consolidate them.
// 4、分别在捕获阶段和冒泡阶段注册事件监听器
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,
);
}
}
}
在 addTrappedEventListener 函数中:
-
首先调用createEventListenerWrapperWithPriority函数来构造事件监听器listener。
// 1、构造事件监听器 let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, );
-
然后初始化变量isPassiveListener,该变量将用来区分事件监听器是被动事件监听器还是非被动事件监听器。
// 2、初始化被动事件监听器 let isPassiveListener = undefined; // 某些浏览器(特别是Chrome和Firefox)已将文档级节点Window,Document和Document.body上的touchstart和touchmove事件的Passive选项的默认值更改为true if (passiveBrowserEventsSupported) { // Browsers introduced an intervention, making these events // passive by default on document. React doesn't bind them // to document anymore, but changing this now would undo // the performance wins from the change. So we emulate // the existing behavior manually on the roots now. // github.com/facebook/re… if ( domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel' ) { isPassiveListener = true; // 设置为被动事件监听器 } }
-
接着获取事件监听的目标对象,是根DOM容器还是目标DOM元素。
// 3、获取绑定事件的目标容器,是根DOM容器还是目标元素 targetContainer = enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport ? (targetContainer: any).ownerDocument : targetContainer;
-
最后根据参数isCapturePhaseListener,分别在捕获阶段和冒泡阶段注册事件监听器。在捕获阶段和冒泡阶段注册事件监听器时,根据isPassiveListener将事件注册为被动事件监听器和非被动事件监听器。
// 4、分别在捕获阶段和冒泡阶段注册事件监听器 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, ); } }
在注册事件监听器时,分别调用了 addEventCaptureListenerWithPassiveFlag、addEventCaptureListener、addEventBubbleListenerWithPassiveFlag、addEventBubbleListener 这几个函数,接下来看看这几个函数。
原生事件注册
// packages/react-dom/src/events/EventListener.js
// 在冒泡阶段 注册原生事件
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
// target.addEventListener(type, listener, useCapture);
// useCapture 为boolean 值,指定事件是否在捕获或冒泡阶段触发
// true - 事件句柄在捕获阶段执行
// false- 事件句柄在冒泡阶段执行
target.addEventListener(eventType, listener, false);
return listener;
}
// 在捕获阶段 注册原生事件
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
// target.addEventListener(type, listener, useCapture);
// useCapture 为boolean 值,指定事件是否在捕获或冒泡阶段触发
// true - 事件句柄在捕获阶段执行
// false- 事件句柄在冒泡阶段执行
target.addEventListener(eventType, listener, true);
return listener;
}
// 在捕获阶段 注册被动事件监听器
export function addEventCaptureListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
// target.addEventListener(type, listener, options);
// options:指定有关 listener 属性的可选参数对象,可用的选项如下
// capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
// once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
// passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
// 在冒泡阶段 注册被动事件监听器
export function addEventBubbleListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
// target.addEventListener(type, listener, options);
// options:指定有关 listener 属性的可选参数对象,可用的选项如下
// capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
// once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
// passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。
target.addEventListener(eventType, listener, {
passive,
});
return listener;
}
addEventBubbleListener:在冒泡阶段,将事件注册为非被动事件监听器。
addEventCaptureListener:在捕获阶段,将事件注册为非被动事件监听器。
addEventCaptureListenerWithPassiveFlag:在捕获阶段,将事件注册为被动事件监听器。
addEventBubbleListenerWithPassiveFlag:在冒泡阶段,将事件注册为被动事件监听器。
这四个函数,内部都是调用了 addEventListener 来监听原生事件,只是传参不同而已。
至此,注册在根DOM容器(div#root) 和目标DOM元素上的原生事件都完成了事件监听。
流程图
原生listener
在事件注册函数addEventListener中,需要传入一个事件监听函数listener,这个监听函数是通过 createEventListenerWrapperWithPriority 函数产生的。
createEventListenerWrapperWithPriority
// packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 根据事件名称获取事件的优先级
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,
);
}
可以看到,不同的domEventName调用getEventPriority函数后会返回不同的事件优先级,最终会有三种情况:
-
DiscreteEventPriority
-
离散事件,如 click、keydown、focusin 等,这些事件的触发不是连续的,优先级最高
-
对应的listener是 dispatchDiscreteEvent
-
ContinuousEventPriority
-
阻塞事件,如 drag、mousemove、scroll 等,这些事件的特点是连续触发,会阻塞渲染,优先级为适中
-
对应的listener是 dispatchContinuousEvent
-
DefaultEventPriority
-
如 load、animation 等事件,优先级最低
-
对应的listener是 dispatchEvent
接下来我们来看看这三种情况所对应的 listener 。
dispatchDiscreteEvent
// packages/react-dom/src/events/ReactDOMEventListener.js
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
// 获取当前 update 的更新优先级
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 0;
try {
// 将当前 update 的更新优先级重置为 DiscreteEventPriority(事件的最高优先级)
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
// 将当前 update 的更新优先级重置为 previousPriority(update 原先的更新优先级)
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
可以看到,dispatchDiscreteEvent 的实现并不复杂,它做的事情如下:
-
获取当前 update 的更新优先级;
-
将当前 update 的更新优先级重置为 DiscreteEventPriority (事件的最高优先级);
-
接着调用 dispatchEvent;
-
最后再次将当前 update 的更新优先级重置为 previousPriority (update 原先的更新优先级);
dispatchContinuousEvent
// packages/react-dom/src/events/ReactDOMEventListener.js
function dispatchContinuousEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
// 获取当前 update 的更新优先级
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 0;
try {
// 将当前 update 的更新优先级重置为 ContinuousEventPriority(事件的中优先级)
setCurrentUpdatePriority(ContinuousEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
// 将当前 update 的更新优先级重置为 previousPriority(update 原先的更新优先级)
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
可以看到,dispatchContinuousEvent 的实现并不复杂,它做的事情如下:
-
获取当前 update 的更新优先级;
-
将当前 update 的更新优先级重置为 ContinuousEventPriority(事件的中优先级)
-
接着调用 dispatchEvent;
-
最后再次将当前 update 的更新优先级重置为 previousPriority (update 原先的更新优先级);
dispatchDiscreteEvent 和 dispatchContinuousEvent 实际上都是对 dispatchEvent 的包装,接下来,我们重点分析这个函数。
事件触发
当原生事件触发之后,首先会进入 dispatchEvent 这个函数。dispatchEvent函数是react事件系统中最关键的函数。
dispatchEvent
// packages/react-dom/src/events/ReactDOMEventListener.js
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}
// TODO: replaying capture phase events is currently broken
// because we used to do it during top-level native bubble handlers
// but now we use different bubble and capture handlers.
// In eager mode, we attach capture listeners early, so we need
// to filter them out until we fix the logic to handle them correctly.
const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
if (
allowReplay &&
hasQueuedDiscreteEvents() &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
// If we already have a queue of discrete events, and this is another discrete
// event, then we can't dispatch it regardless of its target, since they
// need to dispatch in order.
// 按顺序发送离散事件 (离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级最高)
// queueDiscreteEvent 将会创建一个可重播事件,并将其添加到「要重播的DiscreteEvent队列」中
queueDiscreteEvent(
null, // Flags that we're not actually blocked on anything as far as we know.
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
// 尝试调度事件,如果被阻止,则返回 SuspenseInstance 或 Container
let blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn === null) {
// We successfully dispatched this event.
if (allowReplay) {
// 将连续触发类型的事件重置为无事件
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}
if (allowReplay) {
if (
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
// This this to be replayed later once the target is available.
// queueDiscreteEvent 将会创建一个可重播事件,并将其添加到「要重播的DiscreteEvent队列」中
queueDiscreteEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
// 将连续触发类型的事件重置为无事件
clearIfContinuousEvent(domEventName, nativeEvent);
}
if (
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
// 尝试调度事件,如果被阻止,则返回 SuspenseInstance 或 Container
const nextBlockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn) {
// 执行原生事件的 stopPropagation 方法,阻止事件在捕获阶段冒泡阶段进一步传播 (传播意味着向上冒泡到父元素或向下捕获到子元素)
nativeEvent.stopPropagation();
return;
}
}
// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
// 通过插件系统,派发事件
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
dispatchEvent函数的实现代码比较多,我们重点关注在dispatchEvent函数中调用的attemptToDispatchEvent函数和dispatchEventForPluginEventSystem函数:
- attemptToDispatchEvent函数是尝试调度事件,如果调度事件失败,则返回 SuspenseInstance 或 根DOM容器。
- dispatchEventForPluginEventSystem函数的作用,是通过React的事件插件系统来派发事件。
attemptToDispatchEvent -- 关联fiber
// packages/react-dom/src/events/ReactDOMEventListener.js
// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
// 尝试调度事件。 如果被阻止,则返回 SuspenseInstance 或 Container。
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.
// 1、获取实际触发事件的原生DOM
const nativeEventTarget = getEventTarget(nativeEvent);
// 2、获取与原生DOM对应的fiber节点
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// fiber树已经被卸载、SuspenseComponent类型的组件、根节点、原生DOM对应的fiber节点最近的fiber节点和原生DOM对应的fiber节点,上述四种情况下将 targetInst 置为 null
if (targetInst !== null) {
// 从fiber树上获取与原生DOM对应的fiber节点最近的fiber节点
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
// fiber 树已经被卸载
targetInst = null;
} else {
// 获取组件的类型
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {// 组件类型为 SuspenseComponent
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
// Queue the event to be replayed later. Abort dispatching since we
// don't want this event dispatched twice through the event system.
// TODO: If this is the first discrete event in the queue. Schedule an increased
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === HostRoot) { // 根节点
const root: FiberRoot = nearestMounted.stateNode;
if (root.isDehydrated) {
// 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) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
}
}
// 3、通过插件系统,派发事件
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// We're not blocked on anything.
return null;
}
attemptToDispatchEvent函数的作用是尝试调度事件,如果调度事件失败,则返回 SuspenseInstance 或 根DOM容器。在 attemptToDispatchEvent 中,主要做了3件事:
1、定位触发事件的原生DOM节点
const nativeEventTarget = getEventTarget(nativeEvent);
2、获取与原生DOM节点对应的fiber节点
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
在获取与原生DOM节点对应的fiber节点的过程中,如果fiber树已经被卸载、组件是SuspenseComponent类型、组件是根节点、原生DOM对应的fiber节点最近的fiber节点和原生DOM对应的fiber节点不是同一个节点,这四种情况下targetInst 将会被重置为 null 。
通过上面两步,将原生事件和 fiber树关联了起来。
3、通过事件插件系统,派发事件
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
dispatchEventForPluginEventSystem
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = ((targetContainer: any): Node);
// If we are using the legacy FB support flag, we
// defer the event to the null with a one
// time event listener so we can defer the event.
// 兼容旧版 FB support flag,使用一个事件监听器
if (
enableLegacyFBSupport &&
// If our event flags match the required flags for entering
// FB legacy mode and we are processing the "click" event,
// then we can defer the event to the "document", to allow
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
// 兼容 React < 16 的版本
// click 事件注册到 document 上
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
) {
// 在 document 上注册事件监听器
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
}
if (targetInst !== null) {
// The below logic attempts to work out if we need to change
// the target fiber to a different ancestor. We had similar logic
// in the legacy event system, except the big difference between
// systems is that the modern event system now has an event listener
// attached to each React Root and React Portal Root. Together,
// the DOM nodes representing these roots are the "rootContainer".
// To figure out which ancestor instance we should use, we traverse
// up the fiber tree from the target instance and attempt to find
// root boundaries that match that of our current "rootContainer".
// If we find that "rootContainer", we find the parent fiber
// sub-tree for that root and make that our ancestor instance.
let node = targetInst;
// 从当前触发事件的DOM节点开始,遍历fiber树,找到根DOM容器
mainLoop: while (true) {
if (node === null) {
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
// The target is a portal, but it's not the rootContainer we're looking for.
// Normally portals handle their own events all the way down to the root.
// So we should be able to stop now. However, we don't know if this portal
// was part of *our* root.
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)
) {
// This is the rootContainer we're looking for and we found it as
// a parent of the Portal. That means we can ignore it because the
// Portal will bubble through to us.
return;
}
}
grandNode = grandNode.return;
}
}
// Now we need to find it's corresponding host fiber in the other
// tree. To do this we can use getClosestInstanceFromNode, but we
// need to validate that the fiber is a host instance, otherwise
// we need to traverse up through the DOM till we find the correct
// node that is from the other tree.
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
// 批量处理
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
dispatchEventForPluginEventSystem函数的作用是通过事件插件系统派发事件。在该函数中,为了兼容16以下的版本,会把click事件监听器注册到 document 上。然后从当前触发事件的DOM节点开始,向上遍历fiber树,找到根DOM容器,最后调用dispatchEventsForPlugins函数来派发事件。
dispatchEventsForPlugins
// packages/react-dom/src/events/DOMPluginEventSystem.js
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
// 1、定位触发事件的原生DOM节点
const nativeEventTarget = getEventTarget(nativeEvent);
// 2、批量处理队列
const dispatchQueue: DispatchQueue = [];
// 3、收集 所有的listener
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// 4、执行派发
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
在 dispatchEventsForPlugins 函数中,做了4件事情:
-
首先调用getEventTarget函数获取触发事件的原生DOM节点。
-
然后定义dispatchQueue队列,用于存储所有的listener。
-
接着调用extractEvents方法收集所有的listener,存入dispatchQueue队列中。
-
最后调用processDispatchQueue函数,执行事件派发。
接下来,我们来看看收集listener的函数extractEvents。
extractEvents -- 收集listener
// packages/react-dom/src/events/DOMPluginEventSystem.js
// TODO: remove top-level side effect.
// 通过插件注册事件
// 原生DOM事件名称与React事件名称映射关系
// SimpleEventPlugin 是事件系统的基本功能,是事件系统的核心
// 其它插件本质上是 polyfills,在没有这些 polyfill 插件的情况下可允许发布 React 的构建
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
// TODO: we should remove the concept of a "SimpleEventPlugin".
// This is the basic functionality of the event system. All
// the other plugins are essentially polyfills. So the plugin
// should probably be inlined somewhere and have its logic
// be core the to event system. This would potentially allow
// us to ship builds of React without the polyfilled plugins below.
// SimpleEventPlugin 是事件系统的基本功能,是事件系统的核心
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
// We don't process these events unless we are in the
// event's native "bubble" phase, which means that we're
// not in the capture phase. That's because we emulate
// the capture phase here still. This is a trade-off,
// because in an ideal world we would not emulate and use
// the phases properly, like we do with the SimpleEvent
// plugin. However, the plugins below either expect
// emulation (EnterLeave) or use state localized to that
// plugin (BeforeInput, Change, Select). The state in
// these modules complicates things, as you'll essentially
// get the case where the capture phase event might change
// state, only for the following bubble event to come in
// later and not trigger anything as the state now
// invalidates the heuristics of the event plugin. We
// could alter all these plugins to work in such ways, but
// that might cause other unknown side-effects that we
// can't foresee right now.
if (shouldProcessPolyfillPlugins) {
// 下面的几类插件本质上是 polyfills,在没有这些 polyfill 插件的情况下可允许发布 React 的构建
EnterLeaveEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
ChangeEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
SelectEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
BeforeInputEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
}
在 DOMPluginEventSystem.js 文件的 extractEvents 函数中,主要调用了React的 5 种事件插件来收集listener。其中 SimpleEventPlugin 插件是React事件系统的核心,提供了React事件系统的基本功能,其它四个事件插件本质上是 polyfills,在没有这些 polyfill 插件的情况下可允许发布 React 的构建。
接下来我们重点关注 SimpleEventPlugin 插件。
SimpleEventPlugin.extractEvents
// packages/react-dom/src/events/plugins/SimpleEventPlugin.js
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
// 合成事件构造函数
let SyntheticEventCtor = SyntheticEvent;
// react 事件系统中的事件类型
let reactEventType: string = domEventName;
switch (domEventName) {
case 'keypress':
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
return;
}
/* falls through */
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent; // 键盘合成事件
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent; // 焦点合成事件
break;
case 'focusout':
reactEventType = 'blur';
SyntheticEventCtor = SyntheticFocusEvent; // 焦点合成事件
break;
case 'beforeblur':
case 'afterblur':
SyntheticEventCtor = SyntheticFocusEvent; // 焦点合成事件
break;
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}
/* falls through */
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup':
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent; // 鼠标合成事件
break;
case 'drag':
case 'dragend':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'dragstart':
case 'drop':
SyntheticEventCtor = SyntheticDragEvent; // 拖拽合成事件
break;
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent; // 移动端触摸合成事件
break;
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent; // 动画合成事件
break;
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent; // 动画合成事件
break;
case 'scroll':
SyntheticEventCtor = SyntheticUIEvent; // 滚动合成事件
break;
case 'wheel':
SyntheticEventCtor = SyntheticWheelEvent; // 滚轮合成事件
break;
case 'copy':
case 'cut':
case 'paste':
SyntheticEventCtor = SyntheticClipboardEvent; // 复制/粘贴/剪切 合成事件
break;
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
case 'pointerdown':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerup':
SyntheticEventCtor = SyntheticPointerEvent;
break;
default:
// Unknown event. This is used by createEventHandle.
break;
}
// 捕获阶段
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
if (
enableCreateEventHandleAPI &&
eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
) {
// 捕获阶段
// 收集所有监听该事件的 listener
const listeners = accumulateEventHandleNonManagedNodeListeners(
// TODO: this cast may not make sense for events like
// "focus" where React listens to e.g. "focusin".
((reactEventType: any): DOMEventName),
targetContainer,
inCapturePhase,
);
if (listeners.length > 0) {
// Intentionally create event lazily.
// 构造合成事件, 添加到派发队列
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
} else {
// Some events don't bubble in the browser.
// In the past, React has always bubbled them, but this can be surprising.
// We're going to try aligning closer to the browser behavior by not bubbling
// them in React either. We'll start by not bubbling onScroll, and then expand.
const accumulateTargetOnly =
!inCapturePhase &&
// TODO: ideally, we'd eventually add all events from
// nonDelegatedEvents list in DOMPluginEventSystem.
// Then we can remove this special list.
// This is a breaking change that can wait until React 18.
domEventName === 'scroll';
// 冒泡阶段
// 收集节点上所有监听该事件的 listener
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
if (listeners.length > 0) {
// Intentionally create event lazily.
// 构造合成事件, 添加到派发队列
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
}
}
在SimpleEventPlugin.extractEvents函数中:
-
首先将SyntheticEvent赋值给SyntheticEventCtor,SyntheticEvent是React内部的对象(构造函数),是原生事件的跨浏览器包装器,拥有和浏览器原生事件相同的接口(stopPropagation、preventDefault),抹平了不同浏览器的事件兼容性问题。
-
然后根据原生事件名称,初始化相应的合成事件构造函数 SyntheticEventCtor。
-
接着分别在捕获阶段和冒泡阶段收集节点上所有监听该事件的 listener。
-
最后通过合成事件构造函数 SyntheticEventCtor 构造合成事件,将合成事件添加到派发事件队列中。
我们接下来要看的函数是accumulateSinglePhaseListeners,该函数的作用是收集节点上所有监听该事件的 listener。
accumulateSinglePhaseListeners
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
nativeEvent: AnyNativeEvent,
): Array<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
let listeners: Array<DispatchListener> = [];
let instance = targetFiber;
let lastHostComponent = null;
// Accumulate all instances and listeners via the target -> root path.
// 从 targetFiber 开始,向上遍历,直到 root 位置
while (instance !== null) {
const {stateNode, tag} = instance;
// Handle listeners that are on HostComponents (i.e. <div>)
// 处理 原生标签上的listener
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
// createEventHandle listeners
if (enableCreateEventHandleAPI) {
const eventHandlerListeners = getEventHandlerListeners(
lastHostComponent,
);
if (eventHandlerListeners !== null) {
eventHandlerListeners.forEach(entry => {
if (
entry.type === nativeEventType &&
entry.capture === inCapturePhase
) {
listeners.push(
createDispatchListener(
instance,
entry.callback,
(lastHostComponent: any),
),
);
}
});
}
}
// Standard React on* listeners, i.e. onClick or onClickCapture
// 获取标准的监听函数, 如 onClick、onClickCapture等
if (reactEventName !== null) {
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
} else if (
enableCreateEventHandleAPI &&
enableScopeAPI &&
tag === ScopeComponent &&
lastHostComponent !== null &&
stateNode !== null
) {
// Scopes
const reactScopeInstance = stateNode;
const eventHandlerListeners = getEventHandlerListeners(
reactScopeInstance,
);
if (eventHandlerListeners !== null) {
eventHandlerListeners.forEach(entry => {
if (
entry.type === nativeEventType &&
entry.capture === inCapturePhase
) {
listeners.push(
createDispatchListener(
instance,
entry.callback,
(lastHostComponent: any),
),
);
}
});
}
}
// If we are only accumulating events for the target, then we don't
// continue to propagate through the React fiber tree to find other
// listeners.
// 如果只收集目标节点的 listener,则不用向上遍历,直接退出
if (accumulateTargetOnly) {
break;
}
// If we are processing the onBeforeBlur event, then we need to take
// into consideration that part of the React tree might have been hidden
// or deleted (as we're invoking this event during commit). We can find
// this out by checking if intercept fiber set on the event matches the
// current instance fiber. In which case, we should clear all existing
// listeners.
if (enableCreateEventHandleAPI && nativeEvent.type === 'beforeblur') {
// $FlowFixMe: internal field
const detachedInterceptFiber = nativeEvent._detachedInterceptFiber;
if (
detachedInterceptFiber !== null &&
(detachedInterceptFiber === instance ||
detachedInterceptFiber === instance.alternate)
) {
listeners = [];
}
}
// 取父节点,向上遍历
instance = instance.return;
}
return listeners;
}
accumulateSinglePhaseListeners 函数的代码虽然看着很长,但其实它只做了一件事情:从触发事件的DOM节点对应的fiber节点开始,向上遍历,直到根DOM容器(div#root),在这过程中,收集所有的listener,最后将收集的listener返回出去。
接下来我们来看看React是如何将原生事件构造成合成事件的。
SyntheticEventCtor -- 构造合成事件
SyntheticEventCtor 是一个构造函数,根据事件的不同,它有不同的实现方式。在SimpleEventPlugin.extractEvents函数中,就根据不同的事件,赋予了相应事件的处理函数。
switch (domEventName) {
case 'keypress':
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
return;
}
/* falls through */
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent; // 键盘合成事件
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent; // 焦点合成事件
break;
case 'focusout':
reactEventType = 'blur';
SyntheticEventCtor = SyntheticFocusEvent; // 焦点合成事件
break;
case 'beforeblur':
case 'afterblur':
SyntheticEventCtor = SyntheticFocusEvent; // 焦点合成事件
break;
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}
/* falls through */
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup':
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent; // 鼠标合成事件
break;
case 'drag':
case 'dragend':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'dragstart':
case 'drop':
SyntheticEventCtor = SyntheticDragEvent; // 拖拽合成事件
break;
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent; // 移动端触摸合成事件
break;
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent; // 动画合成事件
break;
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent; // 动画合成事件
break;
case 'scroll':
SyntheticEventCtor = SyntheticUIEvent; // 滚动合成事件
break;
case 'wheel':
SyntheticEventCtor = SyntheticWheelEvent; // 滚轮合成事件
break;
case 'copy':
case 'cut':
case 'paste':
SyntheticEventCtor = SyntheticClipboardEvent; // 复制/粘贴/剪切 合成事件
break;
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
case 'pointerdown':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerup':
SyntheticEventCtor = SyntheticPointerEvent;
break;
default:
// Unknown event. This is used by createEventHandle.
break;
}
在 packages/react-dom/src/events/SyntheticEvent.js 文件中,我们可以看到,这些不同的事件处理函数都是调用 createSyntheticEvent 函数创建的新函数,只是传入的EventInterfaceType 不一样而已。
createSyntheticEvent
// packages/react-dom/src/events/SyntheticEvent.js
// This is intentionally a factory so that we have different returned constructors.
// If we had a single constructor, it would be megamorphic and engines would deopt.
function createSyntheticEvent(Interface: EventInterfaceType) {
/**
* Synthetic events are dispatched by event plugins, typically in response to a
* top-level event delegation handler.
*
* These systems should generally use pooling to reduce the frequency of garbage
* collection. The system should check `isPersistent` to determine whether the
* event should be released into the pool after being dispatched. Users that
* need a persisted event should invoke `persist`.
*
* Synthetic events (and subclasses) implement the DOM Level 3 Events API by
* normalizing browser quirks. Subclasses do not necessarily have to implement a
* DOM interface; custom application-specific events can also subclass this.
*/
// 合成事件构造函数
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber,
nativeEvent: {[propName: string]: mixed},
nativeEventTarget: null | EventTarget,
) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
this[propName] = nativeEvent[propName];
}
}
const defaultPrevented =
nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
Object.assign(SyntheticBaseEvent.prototype, {
// 在合成事件构造函数的原型上添加 原生事件的 preventDefault 方法,用于阻止事件的默认行为
preventDefault: function() {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}
// 阻止事件默认行为的浏览器兼容
if (event.preventDefault) {
event.preventDefault();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
// 在合成事件构造函数的原型上添加 原生事件的 stopPropagation 方法,用于阻止事件冒泡到父元素
stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}
// 阻止事件冒泡的浏览器兼容
if (event.stopPropagation) {
event.stopPropagation();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
/**
* We release all dispatched `SyntheticEvent`s after each event loop, adding
* them back into the pool. This allows a way to hold onto a reference that
* won't be added back into the pool.
*/
persist: function() {
// Modern event system doesn't use pooling.
},
/**
* Checks if this event should be released back into the pool.
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: functionThatReturnsTrue,
});
return SyntheticBaseEvent;
}
createSyntheticEvent 是一个工厂函数,不同合成事件的处理函数都是通过它构造的。
在 createSyntheticEvent 函数中,定义了一个合成事件的的构造函数SyntheticBaseEvent。除了 _reactName、_targetInst、type、nativeEvent、target、currentTarget 这些基本的实例属性,在构造相应的合成事件处理函数时把原生事件的属性也添加到了实例对象上。相关代码如下:
// 合成事件的基本属性
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
// 原生事件的属性添加到实例对象上
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
this[propName] = nativeEvent[propName];
}
}
接着使用Object.asign() 方法在SyntheticBaseEvent的原型上添加了 preventDefault 和 stopPropagation 两个方法,使得React的合成事件拥有和浏览器原生事件相同的接口,并抹平了不同浏览器之间的事件兼容性问题,使得开发体验更好。代码如下:
Object.assign(SyntheticBaseEvent.prototype, {
// 在合成事件构造函数的原型上添加 原生事件的 preventDefault 方法,用于阻止事件的默认行为
preventDefault: function() {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}
// 阻止事件默认行为的浏览器兼容
if (event.preventDefault) {
event.preventDefault();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
// 在合成事件构造函数的原型上添加 原生事件的 stopPropagation 方法,用于阻止事件冒泡到父元素
stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}
// 阻止事件冒泡的浏览器兼容
if (event.stopPropagation) {
event.stopPropagation();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
/**
* We release all dispatched `SyntheticEvent`s after each event loop, adding
* them back into the pool. This allows a way to hold onto a reference that
* won't be added back into the pool.
*/
persist: function() {
// Modern event system doesn't use pooling.
},
/**
* Checks if this event should be released back into the pool.
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: functionThatReturnsTrue,
});
构造完合成事件之后,连同收集的listener,一起放入到 dispatchQueue中,等待派发。接下来我们来看看React的事件派发。
processDispatchQueue -- 执行派发
// packages/react-dom/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);
// event system doesn't use pooling.
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
processDispatchQueue的实现逻辑很简单,遍历dispatchQueue,调用processDispatchQueueItemsInOrder函数,对合成事件进行派发。
processDispatchQueueItemsInOrder
// packages/react-dom/src/events/DOMPluginEventSystem.js
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
// 1、捕获阶段 事件处理
// 注意这里是倒叙遍历 listener
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const {instance, currentTarget, listener} = dispatchListeners[i];
// event.isPropagationStopped()
// stopPropagation() 方法被调用则会返回 true,没调用则会返回 false
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
// 2、冒泡阶段 事件处理
// 注意这里是顺序遍历 listener
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 函数中遍历当前事件的所有listener,执行 executeDispatch 派发事件,在 fiber 节点上绑定的 listener 被执行。
在派发事件的过程中,根据捕获阶段和冒泡阶段的不同,对 dispatchListeners 采取了不同的遍历方式:
- 捕获阶段:从上往下 调用 fiber 树中绑定的回调函数,所以从后往前遍历 dispatchListeners
- 冒泡阶段:从下往上 调用 fiber 树中绑定的回调函数,所以从前往后遍历 dispatchListeners
流程图
至此,React合成事件的触发到这就结束了,我们通过流通回顾一下它的过程:
我们来简单地总结下:
-
在触发事件之前,首先根据当前实际触发事件的DOM元素找到与之对应的fiber节点,将原生DOM与fiber树关联起来。
-
然后通过React的事件插件系统,收集所有的事件监听器listener。
-
接着通过构造函数SyntheticBaseEvent构造合成事件,抹平不同浏览器之间的事件兼容性问题。
-
最后分别在捕获阶段和冒泡阶段通过不同的方式遍历dispatchListeners,执行 executeDispatch 派发事件,在 fiber 节点上绑定的 listener 被执行。
总结
在体验上,React合成事件抹平了浏览器的差异性,使得React开发者不再需要关心浏览器兼容性问题,而专注于业务逻辑,另一方面也达到了事件的统一管理,提高了性能。
在架构上,React合成事件打通了从外部原生事件到内部fiber树的交互渠道,使得react能够感知到浏览器提供的原生事件,进而做出不同的响应,例如修改fiber树,变更视图等。
在实现上,主要分为4个步骤:
-
监听原生事件,将原生DOM元素与fiber树进行关联,对齐原生DOM节点与fiber节点
-
遍历fiber树,通过事件插件系统收集所有监听该事件的listener
-
通过构造函数SyntheticBaseEvent构造该事件的合成事件
-
最后遍历dispatchListeners,执行 executeDispatch 派发事件