一、前言
在react17之前事件系统是将所有合成事件都挂载在document上。通过dispatchEvent代理所有的事件处理逻辑。当事件触发时,会收集当前事件源到根元素的所有同类型事件(此处包括捕获和冒泡)。生成事件列表,最后遍历事件列表,执行事件函数。
react17的主要变化是将事件挂载在容器上(通常为id为app的div上)。而到了react18事件系统上有了较大的改变。今天就让我们一起来看看react18中的事件系统。
为什么有事件系统
在开始正文之前,先来思考一个问题。react为什么会有事件系统?
这是因为,对于不同的浏览器事件存在不同的兼容性。react试图实现一个兼容所有浏览器的框架。为此,react实现了一个兼容全浏览器的事件系统来抹平浏览器之间的差异。
二 、新的事件系统
旧的事件系统存在的问题
function Index() {
const refObj = React.useRef(null)
useEffect(() => {
const handler = () => {
console.log('事件监听')
}
refObj.current.addEventListener('click', handler)
return () => {
refObj.current.removeEventListener('click', handler)
}
}, [])
const handleClick = () => {
console.log('冒泡阶段执行')
}
const handleCaptureClick = () => {
console.log('捕获阶段执行')
}
return <button ref={refObj} onClick={handleClick} onClickCapture={handleCaptureClick} >点击</button>
}
在旧的事件系统中触发button的点击事件打印出的内容将是:事件监听 -> 捕获阶段执行 -> 冒泡阶段执行。这是不符合预期的
在新的事件系统中触发button的点击事件打印出的内容将是:捕获阶段执行 -> 事件监听 -> 冒泡阶段执行。这是符合预期的
为什么会出现这样的问题?
原因就是:在旧的事件系统中事件的捕获和冒泡都是模拟的,本质上都是在冒泡阶段执行的。
那么新的事件系统是如何解决这个问题的呢?
一、事件绑定
在新的事件系统中,createRoot方法会一次性注册完全部的事件
function createRoot (container, options) {
...省略
listenToAllSupportedEvents(rootContainerElement); // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
}
让我们看到listenToAllSupportedEvents方法: 方法中出现了两个变量比较关键:allNativeEvents和nonDelegatedEvents
allNativeEvents保存了大多数的浏览器事件:包括click、keydown等81种
nonDelegatedEvents中保存了在js中不冒泡的事件:包括scroll、load、play等
function listenToAllSupportedEvents (rootContainerElement) {
/* allNativeEvents 是一个 set 集合,保存了大多数的浏览器事件 */
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach(function (domEventName) {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
/* nonDelegatedEvents 保存了 js 中,不冒泡的事件 */
if (!nonDelegatedEvents.has(domEventName)) {
/* 在冒泡阶段绑定事件 */
listenToNativeEvent(domEventName, false, rootContainerElement);
}
/* 在捕获阶段绑定事件 */
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
}
}
我们看到通过listenToNativeEvent方法分别对冒泡和捕获阶段进行事件绑定。
如果是不冒泡的事件,只对该事件进行捕获阶段的绑定;否则会对该事件冒泡和捕获阶段都绑定。我们可以明确的一点是react是通过listenToNativeEvent进行事件绑定的。让我们来看看listenToNativeEvent方法
function listenToNativeEvent (domEventName, isCapturePhaseListener, target) {
...省略
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
省略了其他一些代码我们来到了addTrappedEventListener方法
function addTrappedEventListener (targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
...省略
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);
}
}
}
此处我们关注addEventCaptureListener和addEventBubbleListener。这两个方法就是绑定捕获和冒泡事件的方法
function addEventBubbleListener (target, eventType, listener) {
target.addEventListener(eventType, listener, false);
return listener;
}
function addEventCaptureListener (target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}
此时如果发生点击事件就会触发两次listener(此处的listener就是旧系统中的dispatchEvent,react对不同类型使用了不同的执行函数此处不展开说明,有兴趣可以查看源码)
二、事件触发
接下来我们触发一次点击事件。(此处我们只考虑此处的listener就是旧系统中的dispatchEvent)依次执行dispatchEvent -> dispatchEventForPluginEventSystem -> batchedUpdates -> dispatchEventsForPlugins
其中batchedUpdates是批量更新的逻辑。这里我们不展开讨论。
我们重点关注dispatchEventsForPlugins
function dispatchEventsForPlugins (domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
/* 找到发生事件的元素——事件源 */
var nativeEventTarget = getEventTarget(nativeEvent);
/* 待更新队列 */
var dispatchQueue = [];
/* 找到待执行的事件 */
extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
/* 执行事件 */
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
nativeEventTarget:为事件源
dispatchQueue:为更新队列
extractEvents$5:为找到待执行事件的函数
processDispatchQueue:为执行事件的函数
我们来举一个列子
function Index () {
const handleClick = () => {
console.log('button 冒泡阶段执行')
}
const handleCaptureClick = () => {
console.log('button 捕获阶段执行')
}
const handleParentClick = () => {
console.log('div 冒泡阶段')
}
const handleParentCaptureClick = () => {
console.log('div 捕获阶段')
}
return <div onClick={handleParentClick} onClickCapture={handleParentCaptureClick}>
<button onClick={handleClick} onClickCapture={handleCaptureClick} >点击</button>
</div>
}
当点击button触发点击事件。在捕获和冒泡阶段都会执行dispatchEventsForPlugins函数。因此我打印了捕获和冒泡阶段dispatchQueue的值
捕获阶段
冒泡阶段
我们可以看到dispatchQueue中只有一个元素。包含了event和listeners
event:代表事件源合成的event
listeners:是个对象,包含了三个属性;
currentTarget:发生事件的 DOM 元素。
instance : button 对应的 fiber 元素。
listener :一个数组,存放绑定的事件处理函数本身
通过currentTarget中的instance属性来锁定button的friber元素。接下来通过friber就能找到props中的事件。通过return向上遍历找到所有的同类型的事件。比如onClick或者onCLickCaptrue。如此一来就得到了listener。(用来存放绑定事件本身)。最后执行这个listener数组。这里如果是冒泡阶段则从前开始执行。如果是冒泡阶段则从后开始执行。
因为事件源是react自己合成的。这里如果一个事件中执行了e.stopPropagation那么事件源就能感知到。接下来就可以阻止事件冒泡。
如此依赖就模拟了整个事件流的过程并实现了阻止事件冒泡。
三、总结
最后我们来总结整个过程:
首先事件初始化合成事件 -> 捕获和冒泡分别绑定事件 -> 进行事件收集 -> 执行捕获阶段的事件 -> 执行冒泡阶段的事件
如果本人有写的不对的地方,希望可以在指出,我会实时纠正。有疑惑的也欢迎在评论区讨论。