Appearance
事件订阅
这一步会将 通过事件插件机制 收集到的事件,绑定到 React 的根元素节点上,从而可以通过事件代理的机制去管理所有事件的订阅 和 分发。
这一步主要是在 React.createRoot() 初始化阶段,下面我们先了解一下前置知识
前置知识
- 浏览器的事件体系
事件可以分为捕获与冒泡两个阶段,其事件的起点和终点都是 window 对象,目标对象都是事件触发的元素本身(target) 。那么是否就可以通过事件委托机制,在一个根元素上注册了所有的事件,然后通过 e.target去判断触发事件的目标对象。
React 就是模拟的浏览器事件流的机制,不在元素节点上去注册事件,而是通过将所有的事件全部都注册到 React16 的 document 对象|React17 的 root 对象上,然后通过 e.target 寻找目标 DOM,然后转换成目标的 FiberNode 对象,然以通过不同的 任务优先级去执行事件回调方法。这就是 React 的事件池的概念
通过上述 completeWork 阶段和 React 事件池概念的讲解,我们再回到事件系统的初始化流程去,这时候我们就会发现,在 React 初始化的时候,其不是初始化事件机制,其实也是绑定了所有的事件。
- 事件的 passive 属性
当我们在滚动页面的时候(通常是我们监听 touch 事件的时候 , 如监听 touchstart , touchmove , wheel事件的时候),页面其实会有一个短暂的停顿(大概 200ms),浏览器不知道我们是否要 preventDefault,所以它需要一个延迟来检测。这就导致了我们的滑动显得比较卡顿
从 Chrome 51 开始,passive 属性 被引进了 Chrome,我们可以通过对 addEventListener 的第三个参数设置 { passive: true } 来避免浏览器检测这个我们是否有在 touch 事件的 handler 里调用 preventDefault。在这个时候,如果我们依然调用了 preventDefault,就会在控制台打印一个警告。告诉我们这个 preventDefault 会被忽略。 当我们给 addEventListener 的第三个参数设置了 { passive: true },这个事件监听器就被称为 passive event listener。 从 Chrome 56 开始,如果我们给 document 绑定 touchmove 或者 touchstart 事件的监听器,这个 passive 是会被默认设置为 true 以提高性能,具体 chromestatue 文档。但是我们大多数人并不知道这点,并且依旧调用了 preventDefault。这并不会导致什么页面崩溃级的错误,但是这可能导致我们忽略了一个页面性能优化的点,特别是在移动端这种更加重视性能优化的场景下。
- 事件的 capture 属性
事件的 capture 属性,指的是事件是否在捕获阶段触发,而不是在冒泡阶段触发。
事件的 capture 属性,是在 addEventListener 方法的第三个参数中设置的,其值为 true 时,事件在捕获阶段触发,为 false 时,事件在冒泡阶段触发。
问题和对比
下面我们带着以下几个问题 和 为什么 React 需要设置一套合成事件的体系来慢慢的了解 React18 的合成事件机制
- 为什么 React 需要设置一套合成事件的体系来?
- React18 中合成事件就像原生事件一样,是绑定在目标 DOM 上么?
- React18 中合成事件的触发流程是什么,可以先捕获再冒泡么?
事件的订阅
React18 的事件机制其实主要部分是在初始化阶段进行的,通过函数的自执行过程
listenToAllSupportedEvents
在 React.createRoot() 执行的时候,其调用了一个有关事件的入口方法,即 listenToAllSupportedEvents 方法,其主要是遍历所有的原生事件,然后去绑定到根元素上。这里面有一个关键的变量 allNativeEvents, 存储了所有的原生事件, 那么这个变量是怎么来的呢?
这就涉及到函数的自执行过程,在导入 react-dom 的时候,其根据当前的浏览器环境,去初始化了所有的原生事件,然后将其存储在 allNativeEvents 变量中。在上一章事件插件机制中说明了插件了插件在注册阶段会对合成事件 和 原生事件进行注册,并将所有原生事件存储在 allNativeEvents 变量中。
SimpleEventPlugin.registerEvents();EnterLeaveEventPlugin.registerEvents();ChangeEventPlugin.registerEvents();SelectEventPlugin.registerEvents();BeforeInputEventPlugin.registerEvents();
这样就根据每一个原生事件的名称去进行处理,其整个流程为:
是否是 selectionchange
因为 selectionchange 事件不能绑定到根元素上,需要绑定到 document 上。
是否在 捕获阶段 触发
根据事件的名称判断是否在 捕获阶段 触发, 判断条件是 nonDelegatedEvents , 从而决定 capture 属性的值。
事件优先级
根据事件的名称去判断事件的优先级,判断条件是 const eventPriority = getEventPriority(domEventName);, 这样所有的原生事件都被分为
- DiscreteEventPriority
- ContinuousEventPriority
- DefaultEventPriority
事件回调处理函数
根据事件的优先级决定事件的处理函数,从而获取到事件对应的 listener 函数
js
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}事件绑定
js
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
// 遍历所有的原生事件 去进行根元素的绑定操作
allNativeEvents.forEach((domEventName) => {
if (domEventName !== "selectionchange") {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
// 对于 selectionchange 事件
// 其不能绑定到 根元素上,还是需要绑定到 document上
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent("selectionchange", false, ownerDocument);
}
}
}
}这样通过原生事件的名称找到其对应的绑定节点(div#root 还是 document), 在根据事件的名称判断其事件优先级,从而可以根据优先级决定其对应的回调函数, 即 dispatchEvent 、dispatchContinuousEvent、dispatchDiscreteEvent 这三个函数。从而在对应的绑定节点上注册了对应的回调事件,如 click 事件,其对应的回调函数为 dispatchEvent , 所以其结果就是 document.getElement("#root").addEventListener('click' , dispatchEvent , false)。
下面就可以看事件是如何触发,如何分发的了。
