上一篇文章讲到,react老版本的事件系统,虽然模拟了事件模拟和冒泡,但是其执行的时机,其实都是在冒泡阶段。
如
react事件处理的俘获阶段实则是在冒泡阶段执行的。而新版本的事件系统则处理了这个问题。如
可以看到react事件处理的俘获事件在俘获的阶段执行了,为什么会在第一个执行呢?因为react17开始,在初始化的时候,就已经向根容器注册了所有的事件了,初始化阶段注册的函数比useEffect注册的时机早。。接下来从18版本的源码开始看看新版本的事件系统跟老版本的区别。
在新版本的事件系统中,在craeateRoot执行的时候,就会执行listenToAllSupportedEvents一口气向外层容器注册完 全部事件。
先获取了root容器,然后再执行listenToAllSupportedEvents。
看看listenToAllSupportedEvents如何注册全部事件。
1 allNativeEvents是一个set集合,他保存着81个原生事件。
在上一篇说过,事件插件在初始化的时候就会注册
直接调用registerEvents,他不止会收集react事件跟原生事件的依赖,还会收集所有事件。
用set是因为set不会有重复的值。这样当所有插件注册完毕之后,所有原生事件也就收集完毕了。
2 nonDelegatedEvents存放着所有不会冒泡的事件集合。如pause, scroll。
3 如果事件不冒泡,即只执行listenToNativeEvent(domEventName, true, rootContainerElement);
,如果冒泡,那么就会执行
listenToNativeEvent(domEventName, true, rootContainerElement);
和listenToNativeEvent(domEventName, false, rootContainerElement)
其实这里就可以看出区别了,第二个参数就是控制是否注册冒泡的,true表示注册俘获事件,false表示注册冒泡事件。
4 listenToNativeEvent函数,他最终调用addTrappedEventListener函数来注册函数。
function addTrappedEventListener(){
// 判断事件执行的优先级,返回对应监听器。
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
....
if(isCapturePhaseListener){
// 注册俘获事件
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}else {
// 注册冒泡事件。
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
这个函数比较重要的就是两点,一点是createEventListenerWrapperWithPriority
,他会通过优先级获取listener,也就是最终我们注册到容器上要执行的函数。第二点则是通过isCapturePhaseListener
变量(传入给listenToNativeEvent的第二个值),如果是冒泡就注册冒泡事件,否则注册俘获事件。
根据不同的优先级获取不同的dispatchEvent函数,最后都会通过bind绑定当前事件的名称。也就是说当我们触发事件的时候,最终执行的都是dispatchEvent或者dispatchDiscreteEvent…函数
addEventCaptureListener
& addEventBubbleListener
他们的本质就是调用容器.addEventListener函数,注册事件了。
自此,在createRoot初始化的时候,所有事件注册完毕。此时如果触发一次click事件,那么会执行两次dispatchEvent了,一次是俘获阶段,一次是冒泡阶段,这也是跟16版本不同的地方。
一次click事件的发生,会执行dispatchEvent函数。而该函数最终会执行
batchedUpdates是批量更新的逻辑。主要看看dispatchEventsForPlugins
函数。
主要是四个逻辑。
主要看看extractEvents
函数
他会执行SimpleEventPlugin.extractEvents。
function extractEvents(){
// 获取click对应的react事件onClick
const reactName = topLevelEventsToReactNames.get(domEventName);
let SyntheticEventCtor = SyntheticEvent; //默认事件源
// swtich case根据事件获取对应的事件源
// 根据事件类型获取事件源
switch (domEventName) {
...
case 'click':
SyntheticEventCtor = SyntheticMouseEvent; //click事件源
break;
...
}
// 是否是俘获
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 是否冒泡事件
const accumulateTargetOnly =
!inCapturePhase &&
domEventName === 'scroll';
// 获取真正执行的函数
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
if (listeners.length > 0) {
// 有的话就生成事件源
// Intentionally create event lazily.
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
// push进待更新队列
dispatchQueue.push({event, listeners});
}
}
这个函数的重点就是
accumulateSinglePhaseListeners函数会从当前fiber往上遍历直到root,沿途收集所有需要执行的事件。
如图,先判断要收集的名称是俘获还是冒泡,然后通过while循环向上遍历,获取需要执行的函数listener,然后往数组push一个对象,{instance, listener, currentTarget}
。最后返回数组。
此时的dispatchQueue
大概长这个样
{
event: {...}// react合成的事件源。
listeners: [{instance: 当前fiber, listener: f(), currentTaget: dom},{...},{...}]
}
最后看如何消费dispatchQueue队列。
通常情况下,只有一个事件类型,所有dispatchQueue中只有一个元素,
自此,事件触发阶段完毕。
一次点击,两次dispatchEvent函数触发。
新版本的在初始化的时候,就已经绑定事件了。而老版本是在遍历fiber节点的props遇到事件注册才会向容器绑定事件。
其次就是执行时机的不同,在老版本,所有事件不管是俘获还是冒泡,本质就是在冒泡的时候,遍历fiber树,收集俘获和冒泡的事件,形成事件队列,依次执行,以此模拟事件流。
而在新版本,一次事件的触发会执行两次dispatchEvent,第一次收集所有的俘获事件执行。第二次收集所有的冒泡事件执行。执行时机与原生的俘获冒泡时机相同。
参考掘金的 《react进阶实践指南》