前置阅读资料
阅读下面几篇文章有助于理解今天的内容。但是不阅读也没有关系 ~
正文
在浏览器的世界里,我们通常使用 addEventListener
去绑定事件,对于一个被触发的事件,按照时间的顺序,会先后经历三个阶段:
- 捕获阶段(Capture Phase)
- 目标阶段(Target Phase)
- 冒泡阶段(Bubbing Phase)
相应的,addEventListener
也有相应的入参可以指定我们在哪一个阶段去执行回调函数:
target.addEventListener(type, listener, true); // 监听捕获阶段
target.addEventListener(type, listener); // 监听冒泡阶段
React 的事件系统中也实现了相应的功能,拿点击事件来说,默认的 onClick
事件是监听冒泡阶段的,而我们可以使用 onClickCapture
去监听捕获阶段:
<button
className="btn"
onClick={handleClick}
onClickCaputre={handleClickCapture}
>
Hello
</button>
由于两个阶段的触发时机不同,导致了触发的顺序也不同,在 React 内部,也是把两个阶段分开进行处理。
接下来,我们即将分析它的源码,但是在分析之前,我们先看一下上面这一小段 JSX 代码被编译后的模样:
React.createElement("button", {
className: "btn",
onClick: handleClick,
onClickCaputre: handleClickCapture
}, "Hello");
可以看到,onClick
并没有什么特别的,它和 className
一样,就只是作为 props
的一个值被传入,在后面,也和 className
一样,再把它的值存储在此元素对应的 Fiber 节点上。等到用到的时候就从 Fiber 节点上读取。 那它是怎么做到在点击的时候被执行的呢?别着急,看完这篇文章你就理解了。
React 的整个事件系统嵌套的函数还是比较多的,为了避免大家看晕,可以先大概看一下它的流程:
// 0. 在根节点上进行事件委托
app.addEventListener('click', (e) => {
// 当事件被触发时:
// 1. 找到 e.target 属性找到当前点击的 DOM 元素
// 2. 根据当前的 DOM 元素找到对应的 Fiber 节点
// 3. 收集 Fiber 节点到根节点这一链路上的 onClick 函数
// 4. 派发收集到的这些函数
})
下面我们正式进入源码解读的部分。
当我们调用 React.render
或者 React.createRoot
初始化应用的时候,我们首先会创建应用的根级 Fiber 节点,随后就会调用 listenToAllSupportedEvents
来绑定事件:
// rootContainerElement 是我们应用根节点所对应的 DOM 元素
listenToAllSupportedEvents(rootContainerElement);
listenToAllSupportedEvents
会遍历 allNativeEvents
这个 Set
对象。allNativeEvents
里面含有一些原生事件的名称,如:'click'、'input'、'focus'。会在刚开始加载 React 源码的时候就被注入进去:
// 这些方法的作用之一就是为 allNativeEvents 注入了一些事件名
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
接下来看 listenToAllSupportedEvents
做了什么。
export function listenToAllSupportedEvents(
rootContainerElement: EventTarget
) {
// 保证事件只注册一次,在下次调用此函数的时候就不注册了
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
// 这是一个 set 结构,里面的值有各种原生事件的名称
allNativeEvents.forEach(domEventName => {
if (domEventName !== 'selectionchange') {
// 有些特殊的事件不需要委托到应用的根节点,
// 如 'cancel'、'load'、'scroll' 等
if (!nonDelegatedEvents.has(domEventName)) {
// 在根节点上绑定对此事件的冒泡阶段的事件委托
listenToNativeEvent(domEventName, false, rootContainerElement);
}
// 在根节点上绑定对此事件的捕获阶段的事件委托
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
}
}
接着,我们看来 listenToNativeEvent
做了什么。
export function listenToNativeEvent(
// 原生事件名
domEventName: DOMEventName,
// true 的话,就是捕获阶段,反之,冒泡阶段
isCapturePhaseListener: boolean,
// 添加事件委托的节点,目前来说,就是根节点
target: EventTarget,
): void {
let eventSystemFlags = 0;
// 是否标记为捕获阶段
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
这一段代码是比较直观的,它在最后调用了 addTrappedEventListener
,这个函数的作用就是为我们的根节点绑定事件。
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 创建事件委托的回调函数
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let unsubscribeListener;
// 根据捕获阶段还是冒泡阶段,会调用两个不同的函数
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
addEventCaptureListener
和 addEventBubbleListener
非常的相像,就只是传给 addEventListener
的最后的参数不一样,这不禁让我想问,为什么不再开放一个参数,写成一个函数呢?
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
为了不影响主流程,在上面的 addTrappedEventListener
中, 我省略了对 isPassiveListener
判断的讲解,它们的大意就是:如果浏览器的 addEventListener
支持 passive
这个属性,且当前绑定事件名为 'touchstart'、'touchmove'、'wheel' 中的一种,就默认为这些属性的绑定事件增加 passive
,这能很大的提升性能。如果你想了解 passive
的作用,可参考这个回答。
如果你想看 addTrappedEventListenr
的全部内容, 点我查看。
到这里,算是告一段落了,我们已经为根节点绑定好了事件。
接下来,当在页面进行活动,事件被触发了,就会调用回调函数。调用的回调函数就在上面的代码中,叫做 listener
,它是由createEventListenerWrapperWithPriority
生成的。这个函数的代码很直观:
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 根据事件名获取优先级,一般的事件如 'click'、'input' 都是
// DiscreteEventPriority 级别的
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
// 这个返回值就是我们的回调函数 listener
// listenerWrapper 其实接受四个参数,我们当前只绑定了前三个,
// 第四个就是在触发事件的时候,调用回调传入的,
// 也就是 DOM 的 Event 对象。
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
我们以 dispatchDiscreteEvent
为例看后面的流程。事实上,我们发现,它也好,dispatchContinuousEvent
也好,最终都还是调用了 dispatchEvent
。
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 0;
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
所以,我们来研究 dispatchEvent
做了什么,为了不影响主流程的阅读,我也省略了其他代码,剩下的,就是它调用了 attemptToDispatchEvent
方法。
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
let blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
attemptToDispatchEvent
方法就是尝试去派发一个事件,成功的时候,会返回 null
,不成功会返回 SuspenseInstance
或者 Container
(当我写下的时候,我也不知道这个是什么,不过这并不影响我们阅读后面的流程)。
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// 获取当前点击的 DOM 节点
// 一般情况下,就是读取 nativeEvent.target
const nativeEventTarget = getEventTarget(nativeEvent);
// 获取 DOM 节点对应的 Fiber 实例
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// 得到的 Fiber 实例可能有一些问题,不是想要的,这里做兼容
// 只是知道有这段逻辑就好,不想看可以直接忽略,
if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
const tag = nearestMounted.tag;
if (tag === 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;
}
}
}
// 收集 Fiber 节点上的事件,并开始派发
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// 正常情况会走到这里,返回 null
return null;
}
再来看上面函数在返回前最后调用的 dispatchEventForPluginEventSystem
:
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
dispatchEventsForPlugins
的逻辑还是比较清晰的:
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
// 拿到当前点击的 DOM 节点
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
// 从当前 Fiber 节点出发,收集整个链路上的事件
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// 依次触发收集到的事件
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
剩下的内容,就主要是 extractEvents
和 processDispatchQueue
这两个函数了。
先来看 extractEvents
。它会调用 SimpleEventPlugin.extractEvents
。
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
你可能知道,React 事件回调函数的参数不是原生的,而是经过它特殊处理的,叫做 SyntheticEvent
,更多的信息,可以前往 官网这一节 阅读。生成 SyntheticEvent
的过程就是在下面这段代码中。
接下来是 SimpleEventPlugin.extractEvents
的代码:
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;
// 根据事件名找到构造器
switch (domEventName) {
// ... 省略了其他判断
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}
default:
break;
}
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 从当前 Fiber 节点出发,根据事件名
// 从当前节点的 props 中收集监听事件,收集完当前节点
// 就去收集它的父节点,一直到顶。
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
if (listeners.length > 0) {
// 创建基本的 EventTarget
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
// 加入到派发队列中
dispatchQueue.push({ event, listeners });
}
}
只要理解了如何遍历 Fiber 树,accumulateSinglePhaseListeners
的代码就比较容易理解了,在这里我就忽略了,有想看的同学 点击这里 直达。
当调用完 extractEvents
,我们的 dispatchQueue
就收集好了所有的回调函数,接下来就需要调用 processDispatchQueue
去依次调用这些函数。这也是我们最后看的一个函数了。
export function processDispatchQueue(
dispatchQueue: DispatchQueue, // 我们前面收集的事件数组
eventSystemFlags: EventSystemFlags, // 包含了是否是捕获阶段的信息
): void {
// true 的话是捕获阶段,这会影响后续事件的执行顺序
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
}
有一点值得注意,dispatchListeners
包含了所有需要执行的回调函数,它的顺序是从当前元素一直添加到根节点,也就是根节点是最后一个。既然是这样,那捕获阶段和冒泡阶段执行的顺序就会不一样,基于这个原因,再来看 processDispatchQueueItemsInOrder
:
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];
// 判断当前是否已经停止冒泡了,是的话,不执行后面的,直接 return
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];
// 判断当前是否已经停止冒泡了,是的话,不执行后面的,直接 return
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
为什么 event.isPropagationStopped
能起到阻止冒泡的作用呢?
因为 React 的事件对象重写了 stopPropagation
方法。此时,所有的 event
都来自于一个父类。只不过,当在事件处理过程中有一个函数执行了 e.stopPropagation
,就会修改父类的 isPropagationStopped
方法,此时后面的函数都会返回 true
。
stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
// 这里也是导致此现象的原因,就是因为在此阻止冒泡了
// https://reactjs.org/blog/2020/08/10/react-v17-rc.html#fixing-potential-issues
event.stopPropagation();
}
this.isPropagationStopped = functionThatReturnsTrue;
}
// ...省略其他部分...
function functionThatReturnsTrue() {
return true;
}
到这里,React 的合成事件的整个流程就分析完了,虽然代码量有点多,但是只要细心一点,理解整个流程还是不算难的。
看完 React 的合成事件机制,为我之前的无知感到懊悔,我之前还以为 React 不是用事件委托做的,并且我还很鄙视自己公司框架的事件模块。
希望本文能帮助你理解它,也非常感谢你阅读。