前言
从 React 17
开始,React 不再将事件处理添加到 document
处,而是将事件添加到渲染 React 树的根 DOM 节点
上,下图是官方的示意图:
从上图中展示了这一变化,当然,无论是在document
还是根 DOM 容器
上监听事件, 都可以归为事件委托(代理)
请注意
:并不是所有的事件都通过事件代理的方式来处理的,对于某些特殊事件
,比如:scroll
、load
,它们是通过listenToNonDelegatedEvent函数进行绑定.
上述特殊事件最大的不同是监听的 DOM 元素不同, 除此之外, 其他地方的实现与正常事件大体一致.本文讨论的是可以被根 DOM 容器
代理的正常事件.
事件绑定
React 在启动时,会调用 createRootImpl 这个方法:
js
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// ... 省略无关代码
if (enableEagerRootListeners) {
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
}
// ... 省略无关代码
}
内部会调用 listenToAllSupportedEvents 函数, 实际上完成了事件代理
js
// ... 省略无关代码
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (enableEagerRootListeners) {
// 1. 节流优化, 保证全局注册只被调用一次
if ((rootContainerElement: any)[listeningMarker]) {
return;
}
(rootContainerElement: any)[listeningMarker] = true;
// 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
allNativeEvents.forEach((domEventName) => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false, // 冒泡阶段监听
((rootContainerElement: any): Element),
null,
);
}
listenToNativeEvent(
domEventName,
true, // 捕获阶段监听
((rootContainerElement: any): Element),
null,
);
});
}
}
这部分的逻辑是:
节流优化
,保证事件全局中只注册一次- 遍历
allNativeEvents
,内部调用listenToNativeEvent
监听冒泡和捕获阶段的事件。allNativeEvents
包括了大量的原生事件名称。说白了也就是在这里对原生事件的冒泡和捕获进行监听
现在,我们来看看 listenToNativeEvent
函数
js
// ... 省略无关代码
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
const listenerSet = getEventListenerSet(target);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
// 利用set数据结构, 保证相同的事件类型只会被注册一次.
if (!listenerSet.has(listenerSetKey)) {
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// 注册事件监听
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
listenerSet.add(listenerSetKey);
}
}
它的内部去调用 addTrappedEventListener
js
// ... 省略无关代码
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 1. 构造listener,这是关键,它实现了把原生事件派发到 React 的体系内
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let unsubscribeListener;
// 2. 注册事件监听
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
// 注册原生事件 冒泡
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
// 注册原生事件 捕获
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
这部分调用链比较长,但最终会执行到 addEventBubbleListener
、addEventCaptureListener
来监听冒泡、捕获阶段的原生事件
然后,在这一过程中,listener
函数起着至关重要的作用,它实现了将原生事件派发到 React 体系内的功能。
比如点击 DOM 触发原生事件, 原生事件最后会被派发到
react
内部的onClick
函数.listener
函数就是这个由外至内
的关键环节.
listener
是通过createEventListenerWrapperWithPriority
函数产生:
js
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 1. 根据优先级设置 listenerWrapper
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
// 2. 返回 listenerWrapper
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
可以看到, 不同的domEventName
调用getEventPriorityForPluginSystem
后返回不同的优先级, 最终会有 3 种情况:
-
DiscreteEvent
: 优先级最高, 包括click, keyDown, input
等事件, 源码- 对应的
listener
是dispatchDiscreteEvent
- 对应的
-
UserBlockingEvent
: 优先级适中, 包括drag, scroll
等事件, 源码- 对应的
listener
是dispatchUserBlockingUpdate
- 对应的
-
ContinuousEvent
: 优先级最低,包括animation, load
等事件, 源码- 对应的
listener
是dispatchEvent
- 对应的
这 3 种listener
实际上都是对dispatchEvent的包装:
js
// ...省略无关代码
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}
const blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
事件触发
当原生事件触发之后, 首先会进入到dispatchEvent
这个回调函数. 而dispatchEvent
函数是react
事件体系中最关键的函数, 其调用链路较长, 核心步骤如图所示:
重点关注其中 3 个核心环节:
attemptToDispatchEvent
SimpleEventPlugin.extractEvents
processDispatchQueue
attemptToDispatchEvent
attemptToDispatchEvent
把原生事件和fiber树
关联起来
js
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// ...省略无关代码
// 1. 定位原生DOM节点
const nativeEventTarget = getEventTarget(nativeEvent);
// 2. 获取与DOM节点对应的fiber节点
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// 3. 通过插件系统, 派发事件
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
return null;
}
它的逻辑:
- 定位原生 DOM 节点: 调用
getEventTarget
- 获取与 DOM 节点对应的 fiber 节点: 调用
getClosestInstanceFromNode
- 通过插件系统, 派发事件: 调用
dispatchEventForPluginEventSystem
extractEvents
dispatchEvent
函数的调用链路中, 通过不同的插件, 处理不同的事件. 其中最常见的事件都会由SimpleEventPlugin.extractEvents
进行处理
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;
let reactEventType: string = domEventName;
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
// 1. 收集所有监听该事件的函数.
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
);
if (listeners.length > 0) {
// 2. 构造合成事件, 添加到派发队列
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({ event, listeners });
}
}
它的核心:
- 收集所有监听该事件的函数. 调用
accumulateSinglePhaseListeners
函数 - 构造合成事件(
SyntheticEvent
, SyntheticEvent, 是react
内部创建的一个对象, 是原生事件的跨浏览器包装器, 拥有和浏览器原生事件相同的接口(stopPropagation
,preventDefault
), 抹平不同浏览器 api 的差异, 兼容性好.) - 添加到派发队列(
dispatchQueue
)
processDispatchQueue
processDispatchQueue
执行真正的事件派发
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);
}
// ...省略无关代码
}
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
// 1. capture事件: 倒序遍历listeners
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 {
// 2. bubble事件: 顺序遍历listeners
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
去遍历dispatchListeners
数组,通过executeDispatch
函数派发事件,在fiber
节点上绑定的listener
函数被执行 - 根据
捕获(capture)
或冒泡(bubble)
的不同, 采取了不同的遍历方式:capture
事件:从上至下
调用fiber树
中绑定的回调函数, 所以倒序
遍历dispatchListeners
.bubble
事件:从下至上
调用fiber树
中绑定的回调函数, 所以顺序
遍历dispatchListeners
.
至此,事件的整个流程完成
总结
SyntheticEvent打通了从外部原生事件
到内部fiber树
的交互渠道, 使得react
能够感知到浏览器提供的原生事件
, 进而做出不同的响应, 修改fiber树
, 变更视图等.
主要分为 3 步:
- 监听原生事件: 找到
DOM元素
对应的fiber元素
- 收集
listeners
: 遍历fiber树
, 收集所有监听本事件的listener
函数. - 派发合成事件: 构造合成事件, 遍历
listeners
进行派发.