为什么要自己实现一套事件机制
由于fiber机制的特点,生成一个fiber节点时,它对应的dom节点有可能还未挂载,onClick这样的事件处理函数作为fiber节点的prop,也就不能直接被绑定到真实的DOM节点上。为此,React提供了一种“顶层注册,事件收集,统一触发”的事件机制。所谓“顶层注册”,其实是在root元素上绑定一个统一的事件处理函数。“事件收集”指的是事件触发时(实际上是root上的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数。“统一触发”发生在收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象。这里有一个重点是绑定到root上的事件监听并非我们写在组件中的事件处理函数,注意这个区别,下文会提到。以上是React事件机制的简述,这套机制规避了无法将事件直接绑定到DOM节点上的问题,并且能够很好地利用fiber树的层级关系来生成事件执行路径,进而模拟事件捕获和冒泡,另外还带来两个非常重要的特性:
对事件进行归类,可以在事件产生的任务上包含不同的优先级
提供合成事件对象,抹平浏览器的兼容性差异
本文会对事件机制进行详细讲解,贯穿一个事件从注册到被执行的生命周期。
事件注册
与之前版本不同,React17的事件是注册到root上而非document,这主要是为了渐进升级,避免多版本的React共存的场景中事件系统发生冲突。
当我们为一个元素绑定事件时,会这样写:
<div onClick={() => {/*do something*/}}>React</div>
这个div节点最终要对应一个fiber节点,onClick则作为它的prop。当这个fiber节点进入render阶段的complete阶段时,名称为onClick的prop会被识别为事件进行处理。
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
...
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
// 如果propKey属于事件类型,则进行事件绑定
ensureListeningTo(rootContainerElement, propKey, domElement);
}
}
}
}
registrationNameDependencies是一个对象,存储了所有React事件对应的原生DOM事件的集合,这是识别prop是否为事件的依据。如果是事件类型的prop,那么将会调用ensureListeningTo去绑定事件。
接下来的绑定过程可以概括为如下几个关键点:
根据React的事件名称寻找该事件依赖,例如onMouseEnter事件依赖了mouseout和mouseover两个原生事件,onClick只依赖了click一个原生事件,最终会循环这些依赖,在root上绑定对应的事件。例如组件中为onClick,那么就会在root上绑定一个click事件监听。
依据组件中写的事件名识别其属于哪个阶段的事件(冒泡或捕获),例如onClickCapture这样的React事件名称就代表是需要事件在捕获阶段触发,而onClick代表事件需要在冒泡阶段触发。
根据React事件名,找出对应的原生事件名,例如click,并根据上一步来判断是否需要在捕获阶段触发,调用addEventListener,将事件绑定到root元素上。
若事件需要更新,那么先移除事件监听,再重新绑定,绑定过程重复以上三步。
经过这一系列过程,事件监听器listener最终被绑定到root元素上。
// 根据事件名称,创建不同优先级的事件监听器。
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
listenerPriority,
);
// 绑定事件
if (isCapturePhaseListener) {
...
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
...
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
事件监听器listener是谁
上面提到的绑定事件的时候,绑定到root上的事件监听函数是listener,然而这个listener并不是我们直接在组件里写的事件处理函数。通过上面的代码可知,listener是createEventListenerWrapperWithPriority的调用结果
为什么要创建这么一个listener,而不是直接绑定写在组件里的事件处理函数呢?
其实createEventListenerWrapperWithPriority这个函数名已经说出了答案:依据优先级创建一个事件监听包装器。有两个重点:优先级和事件监听包装器。这里的优先级是指事件优先级(关于事件优先级的详细介绍请移步React中的优先级 )。
事件优先级是根据事件的交互程度划分的,优先级和事件名的映射关系存在于一个Map结构中。createEventListenerWrapperWithPriority会根据事件名或者传入的优先级返回不同级别的事件监听包装器。
总的来说,会有三种事件监听包装器:
dispatchDiscreteEvent: 处理离散事件
dispatchUserBlockingUpdate:处理用户阻塞事件
dispatchEvent:处理连续事件
这些包装器是真正绑定到root上的事件监听器listener,它们持有各自的优先级,