跳到主要内容

认识

2024年01月25日
柏拉文
越努力,越幸运

一、认识


React.js 17.x 顶层容器为 root。因此, React 执行大多数事件都会调用 rootNode.addEventListener()。触发事件后, 事件流为: Document 捕获 -> React 合成事件捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 合成事件冒泡 -> Document 冒泡

二、对比


2.1 事件绑定位置

原生 DOM 将事件绑定在当前元素上

React 将事件函数存储到当前元素对应的 Fiber 中的 memoizedPropspendingProps

2.2 事件执行顺序

React.js 17 之后, 事件委托的容器为ReactDOM.render(,container)中的container上。触发事件之后, 事件流为: Document 捕获 -> React 事件捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 冒泡 -> Document 冒泡

import { useEffect, useRef } from 'react';

function App() {
const parentRef = useRef(null);
const childRef = useRef(null);

const parentBubble = () => {
console.log('父元素 React 事件冒泡');
};

const parentCapture = () => {
console.log('父元素 React 事件捕获');
};

const childBubble = () => {
console.log('子元素 React 事件冒泡');
};

const childCapture = () => {
console.log('子元素 React 事件捕获');
};

useEffect(() => {
const parent: any = parentRef.current;
const child: any = childRef.current;

if (parent && child) {
parent.addEventListener(
'click',
() => {
console.log('父元素原生捕获');
},
true
);
parent.addEventListener(
'click',
() => {
console.log('父元素原生冒泡');
},
false
);
child.addEventListener(
'click',
() => {
console.log('子元素原生捕获');
},
true
);
child.addEventListener(
'click',
() => {
console.log('子元素原生冒泡');
},
false
);
document.addEventListener(
'click',
() => {
console.log('document 元素捕获');
},
true
);
document.addEventListener(
'click',
() => {
console.log('document 元素冒泡');
},
false
);
}
}, []);

return (
<div>
<div
ref={parentRef}
className="parent"
onClick={parentBubble}
onClickCapture={parentCapture}
>
父元素
<div
ref={childRef}
className="child"
onClick={childBubble}
onClickCapture={childCapture}
>
子元素
</div>
</div>
</div>
);
}

export default App;

点击按钮,执行结果如下:

document 捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document 冒泡

由上可以看出, React.js 17.x 之后, 注册的捕获/冒泡事件是真正的在冒泡/捕获阶段执行的

2.3 阻止默认事件

React 中,我们的所有事件都可以说是虚拟的,并不是原生的事件。我们在 React 中拿到的事件源(e) 也并非是真正的事件e,而是经过React单独处理的e

  1. 原生事件: 可以通过e.preventDefault()return false来阻止默认事件

  2. 合成事件: 通过e.preventDefault()阻止默认事件, 由于 React 注册的事件处理函数不是真正的处理函数, 所以不可以通过 return false 来组织默认事件。特别注意, 原生事件和合成事件的 e.preventDefault() 并非是同一个函数,React的事件源 e 是单独创立的。

三、绑定事件


React.js 18.x 的事件系统中,在 createRoot 会一口气向外层容器上注册绑定完全部事件。

react/packages/react-dom/src/client/ReactDOMRoot.js
function createRoot(container, options) {
/* 省去和事件无关的代码,通过如下方法注册事件 */
listenToAllSupportedEvents(rootContainerElement);
}

createRoot 中,通过 listenToAllSupportedEvents 注册事件

react/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(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);
}
});
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.
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}

listenToAllSupportedEvents 主要目的就是通过 listenToNativeEvent 绑定浏览器事件。其中, allNativeEvents 是一个 set 集合,保存了 81 个浏览器常用事件。nonDelegatedEvents 也是一个集合,保存了浏览器中不会冒泡的事件,一般指的是媒体事件,比如pauseplayplaying 等,还有一些特殊事件,比如 cancelcloseinvalidloadscroll。 依次循环遍历 allNativeEventsnonDelegatedEvents, 通过 listenToNativeEvent 绑定两个集合中的事件。

listenToNativeEvent 分别对冒泡和捕获阶段进行事件绑定。如果是不冒泡的事件, 只对该事件进行捕获阶段的绑定;否则会对该事件冒泡和捕获阶段都绑定。

react/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}

addTrappedEventListener 主要逻辑如下

react/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let isPassiveListener: void | boolean = undefined;
if (passiveBrowserEventsSupported) {
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}

targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer;

let unsubscribeListener;

if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
// $FlowFixMe[missing-this-annot]
listener = function (...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener,
);
return originalListener.apply(this, p);
};
}
// TODO: There are too many combinations here. Consolidate them.
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,
);
}
}
}

其中, listener 逻辑如下:

react/packages/react-dom-bindings/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,
);
}

因此, listenToNativeEvent 本质上是向原生 DOM 注册绑定事件, 将所有的事件监听函数都保存到 dispatchEvent 中。

四、触发事件


4.1 调度事件

React.js 无论是通过 addEventCaptureListener 还是 addEventBubbleListener, 都会将 dispatchEvent 最为真正的事件处理函数。因此, 当我们触发事件后,首先执行的是 dispatchEvent 函数。根据真实的事件源对象, 找到 e.target 真实的 DOM 元素。然后根据 DOM 元素找到与它对应的 Fiber

export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}

let blockedOn = findInstanceBlockingEvent(nativeEvent);
if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
clearIfContinuousEvent(domEventName, nativeEvent);
return;
}

if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);

if (
eventSystemFlags & IS_CAPTURE_PHASE &&
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,
);
}

4.2 批量更新

接下来, React.js 通过 batchedUpdates 开启批量更新, 并执行传入的 dispatchEventsForPlugins 函数。

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 (
enableLegacyFBSupport &&
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
}
if (targetInst !== null) {
let node: null | Fiber = targetInst;

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) {
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 ||
(enableFloat ? parentTag === HostHoistable : false) ||
parentTag === HostSingleton
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}

batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
export function batchedUpdates(fn, a, b) {
if (isInsideEventHandler) {
return fn(a, b);
}
isInsideEventHandler = true;
try {
return batchedUpdatesImpl(fn, a, b);
} finally {
isInsideEventHandler = false;
finishEventHandler();
}
}

4.3 执行事件插件函数

function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

如上所示, 事件插件函数主要工作如下:

  1. 获取事件源

  2. 收集事件: 从事件源开始逐渐向上,查找元素类型为 HostComponent 对应的 fiber ,收集上面的 React 合成事件。针对捕获事件存储到 capture 数组中, 针对冒泡事件存储到 bubble 数组中

  3. 执行事件队列

4.4 执行事件队列

在捕获阶段执行 capture 数组中的事件回调, 在冒泡阶段执行 bubble 中的事件回调