跳到主要内容

认识

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

一、认识


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

二、对比


2.1 事件绑定位置

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

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

2.2 事件执行顺序

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

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生捕获");
},
true
);
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生冒泡");
},
false
);
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生捕获");
},
true
);
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生冒泡");
},
false
);
document.addEventListener(
"click",
() => {
console.log("document 元素捕获");
},
true
);
document.addEventListener(
"click",
() => {
console.log("document 元素冒泡");
},
false
);
}
parentBubble = () => {
console.log("父元素 React 事件冒泡");
};
parentCapture = () => {
console.log("父元素 React 事件捕获");
};
childBubble = () => {
console.log("子元素 React 事件冒泡");
};
childCapture = () => {
console.log("子元素 React 事件捕获");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parentCapture}
>
<div
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childCapture}
>
事件执行顺序
</div>
</div>
);
}
}

ReactDOM.render(<App />, document.getElementById("root"));

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

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

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

2.3 阻止默认事件

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

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

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

三、绑定事件


3.1 初始化绑定

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 注册绑定事件, 无论是通过 addEventCaptureListener 还是 addEventBubbleListener, 都会将 dispatchEvent 作为真正的事件处理函数。

3.2 事件函数存储

completeWork 阶段, 通过 fiber 创建真实 DOM 节点时, 将 fiber.pendingProps 存储到真实 DOM 节点中。其中包括 onClickonClickCapture 等事件。

四、触发事件


4.1 调度事件

React.js 无论是通过 addEventCaptureListener 还是 addEventBubbleListener, 都会将 dispatchEvent 作为真正的事件处理函数。因此, 当我们触发事件后,首先执行的是 dispatchEvent 函数。

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 中的事件回调