React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的。
本文浅析的React版本为17.0.1,使用ReactDOM.render
创建应用,不含优先级相关。
原理简述
React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot
创建的时候,就会在root
节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身。
同时,React将事件分为3种类型——discreteEvent
、userBlockingEvent
、continuousEvent
,它们拥有不同的优先级,在绑定事件处理函数时会使用不同的回调函数。
React事件建立在原生基础上,模拟了一套冒泡和捕获的事件机制,当某一个DOM元素触发事件后,会冒泡到React绑定在root
节点的处理函数,通过target
获取触发事件的DOM对象和对应的Fiber节点,由该Fiber节点向上层父级遍历,收集一条事件队列,再遍历该队列触发队列中每个Fiber对象对应的事件处理函数,正向遍历模拟冒泡,反向遍历模拟捕获,所以合成事件的触发时机是在原生事件之后的。
Fiber对象对应的事件处理函数依旧是储存在props
里的,收集只是从props
里取出来,它并没有绑定到任何元素上。
源码浅析
以下源码仅为基础逻辑的浅析,旨在理清事件机制的触发流程,去掉了很多流程无关或复杂的代码。
委托事件绑定
这一步发生在调用了ReactDOM.render
过程中,在创建fiberRoot
的时候会在root
节点的DOM元素上监听所有支持的事件。
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// ...
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
// 监听所有支持的事件
listenToAllSupportedEvents(rootContainerElement);
// ...
}
listenToAllSupportedEvents
在绑定事件时,会通过名为allNativeEvents
的Set
变量来获取对应的eventName
,这个变量会在一个顶层函数进行收集,而nonDelegatedEvents
是一个预先定义好的Set
。
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
allNativeEvents.forEach(domEventName => {
// 排除不需要委托的事件
if (!nonDelegatedEvents.has(domEventName)) {
// 冒泡
listenToNativeEvent(
domEventName,
false,
((rootContainerElement: any): Element),
null,
);
}
// 捕获
listenToNativeEvent(
domEventName,
true,
((rootContainerElement: any): Element),
null,
);
});
}
listenToNativeEvent
listenToNativeEvent
函数在绑定事件之前会先将事件名在DOM元素中标记,判断为false
时才会绑定。
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
// ...
// 在DOM元素上储存一个Set用来标识当前元素监听了那些事件
const listenerSet = getEventListenerSet(target);
// 事件的标识key,字符串拼接处理了下
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
if (!listenerSet.has(listenerSetKey)) {
// 标记为捕获
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// 绑定事件
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
// 添加到set
listenerSet.add(listenerSetKey);
}
}
addTrappedEventListener
addTrappedEventListener
函数会通过事件名取得对应优先级的listener
函数,在交由下层函数处理事件绑定。
这个listener
函数是一个闭包函数,函数内能访问targetContainer
、domEventName
、eventSystemFlags
这三个变量。
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 根据优先级取得对应listener
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
if (isCapturePhaseListener) {
addEventCaptureListener(targetContainer, domEventName, listener);
} else {
addEventBubbleListener(targetContainer, domEventName, listener);
}
}
addEventCaptureListener
函数和addEventBubbleListener
函数内部就是调用原生的target.addEventListener
来绑定事件了。
这一步是循环一个存有事件名的Set
,将每一个事件对应的处理函数绑定到root
节点DOM元素上。
不需要委托事件绑定
不需要委托的事件其中也包括媒体元素的事件。
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
'cancel',
'close',
'invalid',
'load',
'scroll',
'toggle',
...mediaEventTypes,
]);
export const mediaEventTypes: Array<DOMEventName> = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'encrypted',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'loadstart',
'pause',
'play',
'playing',
'progress',
'ratechange',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting',
];
这些事件绑定发生在completeWork
阶段,最后会执行setInitialProperties
方法。
setInitialProperties
setInitialProperties
方法里会绑定不需要委托的直接到DOM元素本身,也会设置style
和一些传入的DOM属性。
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
let props: Object;
switch (tag) {
// ...
case 'video':
case 'audio':
for (let i = 0; i < mediaEventTypes.length; i++) {
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
}
props = rawProps;
break;
default:
props = rawProps;
}
// 设置DOM属性,如style...
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
}
switch
里会根据不同的元素类型,绑定对应的事件,这里只留下了video
元素和audio
元素的处理,它们会遍历mediaEventTypes
来将事件绑定在DOM元素本身上。
listenToNonDelegatedEvent
listenToNonDelegatedEvent
方法逻辑和上一节的listenToNativeEvent
方法基本一致。
export function listenToNonDelegatedEvent(
domEventName: DOMEventName,
targetElement: Element,
): void {
const isCapturePhaseListener = false;
const listenerSet = getEventListenerSet(targetElement);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
if (!listenerSet.has(listenerSetKey)) {
addTrappedEventListener(
targetElement,
domEventName,
IS_NON_DELEGATED,
isCapturePhaseListener,
);
listenerSet.add(listenerSetKey);
}
}
值得注意的是,虽然事件处理绑定在DOM元素本身,但是绑定的事件处理函数不是代码中传入的函数,后续触发还是会去收集处理函数执行。
事件处理函数
事件处理函数指的是React中的默认处理函数,并不是代码里传入的函数。
这个函数通过createEventListenerWrapperWithPriority
方法创建,对应的步骤在上文的addTrappedEventListener
中。
createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 从内置的Map中获取事件优先级
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
// 根据优先级不同返回不同的listener
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
createEventListenerWrapperWithPriority
函数里返回对应事件优先级的listener
,这3个函数都接收4个参数。
function fn(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
//...
}
返回的时候bind
了一下传入了3个参数,这样返回的函数为只接收nativeEvent
的处理函数了,但是能访问前3个参数。
dispatchDiscreteEvent
方法和dispatchUserBlockingUpdate
方法内部其实都调用的dispatchEvent
方法。
dispatchEvent
这里删除了很多代码,只看触发事件的代码。
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
// ...
// 触发事件
attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
// ...
}
attemptToDispatchEvent
方法里依然会处理很多复杂逻辑,同时函数调用栈也有几层,我们就全部跳过,只看关键的触发函数。
dispatchEventsForPlugins
dispatchEventsForPlugins
函数里会收集触发事件开始各层级的节点对应的处理函数,也就是我们实际传入JSX
中的函数,并且执行它们。
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
// 收集listener模拟冒泡
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// 执行队列
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents
extractEvents
函数里主要是针对不同类型的事件创建对应的合成事件,并且将各层级节点的listener
收集起来,用来模拟冒泡或者捕获。
这里的代码较长,删除了不少无关代码。
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
// 根据不同的事件来创建不同的合成事件
switch (domEventName) {
case 'keypress':
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case 'click':
// ...
case 'mouseover':
SyntheticEventCtor = SyntheticMouseEvent;
break;
case 'drag':
// ...
case 'drop':
SyntheticEventCtor = SyntheticDragEvent;
break;
// ...
default:
break;
}
// ...
// 收集各层级的listener
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
);
if (listeners.length > 0) {
// 创建合成事件
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
}
accumulateSinglePhaseListeners
accumulateSinglePhaseListeners
函数里就是在向上层遍历来收集一个列表后面会用来模拟冒泡。
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
): Array<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;
let lastHostComponent = null;
// 通过触发事件的fiber节点向上层遍历收集dom和listener
while (instance !== null) {
const {stateNode, tag} = instance;
// 只有HostComponents有listener (i.e. <div>)
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
if (reactEventName !== null) {
// 从fiber节点上的props中获取传入的事件listener函数
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push({
instance,
listener,
currentTarget: lastHostComponent,
});
}
}
}
if (accumulateTargetOnly) {
break;
}
// 继续向上
instance = instance.return;
}
return listeners;
}
最后的数据结构如下:
dispatchQueue
的数据结构为数组,类型为[{ event,listeners }]
。
这个listeners
则为一层一层收集到的数据,类型为[{ currentTarget, instance, listener }]
processDispatchQueue
processDispatchQueue
函数里会遍历dispatchQueue
。
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);
}
}
dispatchQueue
中的每一项在processDispatchQueueItemsInOrder
函数里遍历执行。
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];
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];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
processDispatchQueueItemsInOrder
函数里会根据判断来正向、反向的遍历来模拟冒泡和捕获。
executeDispatch
executeDispatch
函数里会执行listener
。
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
const type = event.type || 'unknown-event';
event.currentTarget = currentTarget;
listener(event);
event.currentTarget = null;
}
结语
本文旨在理清事件机制的执行,按照函数执行栈简单的罗列了代码逻辑,如果不对照代码看是很难看明白的,原理在开篇就讲述了。
React的事件机制隐晦而复杂,根据不同情况做了非常多的判断,并且还有优先级相关代码、合成事件,这里都没有一一讲解,原因当然是我还没看~
平时用React也就写写简单的手机页面,以前老板还经常吐槽加载不够快,那也没啥办法,就对我的工作而言,有没有Cocurrent
都是无关紧要的,这合成事件更复杂,完全就是不需要的,不过React的作者们脑洞还是牛皮,要是没看源码我肯定是想不到竟然模拟了一套事件机制。
小思考
- 为什么原生事件的
stopPropagation
可以阻止合成事件的传递?
这些问题我放以前根本没想过,不过今天看了源码以后才想的。
-
因为合成事件是在原生事件触发之后才开始收集并触发的,所以当原生事件调用
stopPropagation
阻止传递后,根本到不到root
节点,触发不了React绑定的处理函数,自然合成事件也不会触发,所以原生事件不是阻止了合成事件的传递,而是阻止了React中绑定的事件函数的执行。<div 原生onClick={(e)=>{e.stopPropagation()}}> <div onClick={()=>{console.log("合成事件")}}>合成事件</div> </div>
比如这个例子,在原生onClick阻止传递后,控制台连“合成事件”这4个字都不会打出来了。