本文内容基于 React 17.0.0,主要在 legacy mode 下针对 React-DOM 来进行分析
引言
React 的合成事件系统一直是 React 的标志性特性之一,其效果是在开发者和真实事件之间加了一层中间层,面对开发者可以输出符合其设计意图的 API,面对原始事件它可以进行挟持和加工。虚拟 DOM 也采用了类似的分层设计,在开发者和原始 DOM 之间加了一层抽象内容。这类抽象设计方式十分值得去深入学习,并且合成事件系统是了解 React 原理不可或缺的一步,故有了本篇文章。
如有错误,还请不吝斧正。
一致性
为了方便理解并防止分歧,有必要对本文内一些可能混淆的概念进行强调并释义。
请注意,释义仅对本文负责,并不一定适用于本文以外的内容,请注意甄别。
名词表
名词名称 | 释义 |
---|---|
真实事件 | 泛指由开发者写在组件里的事件,试图挂载在 DOM 上的事件,也可理解为真实的事件。 |
原生事件 | 泛指浏览器里存在并会发生的 UI Events,比如 ‘click’、’cancel’ 等。 |
合成事件对象 | 由 React 进行封装过的 Event 对象。 |
合成事件系统 | 泛指整个由 React 来处理事件机制的系统。 |
根 DOM 节点 | 并非指 Document 节点,而是 React 应用挂载的 DOM 节点。 |
事件代理 | 又称事件委托,与事件委托无异,笔者觉着在本文中“代理”这个措辞更合适所以便用了,故特此说明 : ) |
合成事件系统的目的
由于没有找到太官方的答案,所以只能提供一些相对主观的答案:
性能优化:使用事件代理统一接收原生事件的触发,从而可以使得真实 DOM 上不用绑定事件。(但对应的,可能会触发过多的无意义的事件收集。)
分层设计:解决跨平台问题。
合成事件对象:抹平浏览器差异。
挟持事件触发:原生事件的处理都是先通过 React 再交给真实事件,这样 React 可以知道触发了什么原生事件,是什么原生事件调用了对应的真实事件,真实事件内有关 React 相关状态的改动是在什么事件里触发的。
现版本 React 合成事件系统有一个不可替代的原因是,React 需要知道更新是由什么原生事件触发的。
React 挟持事件触发可以知道用户触发了什么事件,是通过什么原生事件调用的真实事件。这样可以通过对原生事件的优先级定义进而确定真实事件的优先级,再进而可以确定真实事件内触发的更新是什么优先级,最终可以决定对应的更新应该在什么时机更新。
**
初始化合成事件系统
初始化部分的工作内容相对简单,核心目的就是生成一些静态变量,方便后面使用。
下面会简单介绍一下重要的静态变量,并不会对其初始化过程进行研究,因为源码为了这点小事写的非常绕。
入口程序在 react-dom/src/events/DOMPluginEventSystem.js#L89-L93,有兴趣的可自行 debug。
这五个 EventPlugin
关系可以这么理解,SimpleEventPlugin
是合成事件系统的基本功能实现,而其他的几个 EventPlugin
只不过是它的 polyfill。
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
eventPriorities
原生事件及其优先级的映射
{
"cancel": 0,
// ...
"drag": 1,
// ...
"abort": 2,
// ...
}
React 对原生事件的优先级定义主要有三类
export const DiscreteEvent: EventPriority = 0; // 离散事件,cancel、click、mousedown 这类单点触发不持续的事件,优先级最低
export const UserBlockingEvent: EventPriority = 1; // 用户阻塞事件,drag、mousemove、wheel 这类持续触发的事件,优先级相对较高
export const ContinuousEvent: EventPriority = 2; // 连续事件,load、error、waiting 这类大多与媒体相关的事件为主的事件需要及时响应,所以优先级最高
topLevelEventsToReactNames
原生事件和合成事件的映射
{
"cancel": "onCancel",
// ...
"pointercancel": "onPointerCancel",
// ...
"waiting": "onWaiting"
}
registrationNameDependencies
合成事件和其依赖的原生事件集合的映射
{
"onCancel": ["cancel"],
"onCancelCapture": ["cancel"],
// ...
"onChange": ["change", "click", "focusin","focusout", "input", "keydown", "keyup", "selectionchange"],
"onCancelCapture": ["change", "click", "focusin","focusout", "input", "keydown", "keyup", "selectionchange"],
"onSelect": ["focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange"],
"onSelectCapture": ["focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange"],
// ...
}
其他
除此之外,还有些不全是动态生成的变量或常量也简单介绍一下:
变量 | 数据类型 | 意义 |
---|---|---|
allNativeEvents | Set |
所有有意义的原生事件名称集合 |
nonDelegatedEvents | Set |
不需要在冒泡阶段进行事件代理(委托)的原生事件名称集合 |
注册事件代理
17.x 相较于 16.x 发生了较大改变,请注意甄别。
React 采用了事件代理去捕获浏览器发生的原生事件,接着会利用原生事件里的 Event
对象去收集真实事件,然后调用真实事件。
收集事件我们后面会讲,我们先关注前半部分“事件代理”,它是在什么阶段做的,怎么做的。
在 17.x 版本中,创建 ReactRoot
阶段便会调用 listenToAllSupportedEvents
函数,并在所有可以监听的原生事件上添加监听事件。
调用链如下图所示:
listenToAllSupportedEvents
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// 入参 rootContainerElement 由创建 ReactRoot 的函数传入,其内容为 React 应用的根 DOM 节点。
// enableEagerRootListeners 为固定不变的标识常量,常为 true,可忽略。
// 其意义是指“尽早的在所有原生事件上添加监听器”这一特性是否开启,与之相对的在 16.x 版本中监听器会在较晚的时机按需添加。
if (enableEagerRootListeners) {
// listeningMarker 是一个由固定字符加随机字符组成的标识,用于标识节点是否已经以 react 的方式在所有原生事件上添加监听事件,
// 如果已经添加过,则直接跳过,节省一些不必要的工作
if ((rootContainerElement: any)[listeningMarker]) {
return;
}
// 添加标识
(rootContainerElement: any)[listeningMarker] = true;
// 遍历所有原生事件
// 除了不需要在冒泡阶段添加事件代理的原生事件,仅在捕获阶段添加事件代理
// 其余的事件都需要在捕获、冒泡阶段添加代理事件
allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false,
((rootContainerElement: any): Element),
null,
);
}
listenToNativeEvent(
domEventName,
true,
((rootContainerElement: any): Element),
null,
);
});
}
}
listenToNativeEvent
// 简化版本
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
// ...
// target 节点上存了一个 Set 类型的值,内部存储着已经添加监听器的原生事件名称,目的是为了防止重复添加监听器。
const listenerSet = getEventListenerSet(target);
// 效果:'cancel' -> 'cancel__capture' | 'cancel__bubble'
// 获取将要放到 listenerSet 里的事件名称
const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
// 如果未绑定则绑定
if (!listenerSet.has(listenerSetKey)) {
// 在现阶段 eventSystemFlags 入参常为 0,所以可以理解为,
// 只要是在捕获阶段添加监听器的添加过程中,eventSystemFlags = IS_CAPTURE_PHASE = 1 << 2。
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
// 添加至 listenerSet
listenerSet.add(listenerSetKey);
}
}
addTrappedEventListener
// 简化版本
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 创建带有优先级的事件监听器,具体内容后面概述
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
// ...
let unsubscribeListener;
// 在原生事件上添加不同阶段的事件监听器
if (isCapturePhaseListener) {
// ...
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
// ...
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
createEventListenerWrapperWithPriority(创建带优先级的监听器)
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 从前文提到的 eventPriorities 中获取当前原生事件的优先级
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
// 根据不同的优先级提供不同的监听函数
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
// 三类监听器的入参其实一样,其函数签名均为:
// (domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent) => void
// 前三个参数由当前函数提供,最后一个参数便是原生监听器会拥有的唯一入参 Event 对象
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
addEventCaptureListener/addEventBubbleListener(挂载监听器)
不赘述内容了
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
小结
最终在根 DOM 节点上,每个原生事件都绑定了其对应优先级所对应的 监听器
触发事件伊始
到此,文章之前的内容都可以理解为合成事件系统的准备工作,直到页面渲染完后成合成事件系统基本没有其他事了。
正常渲染完成后,当浏览器发生了原生事件的调用,合成事件系统才会开始工作👷♂️,前文提到的监听器会接收浏览器发生的事件,然后向下传递信息,收集相关事件,模拟浏览器的触发流程并达成我们预期的触发效果。
那么问题来了,React 的合成事件系统如何实现的呢?
效果总览
在讲解之前先给大家一个整体的概念,留个印象就好,后面讲解的时候可以对照着看。
测试 Demo 如下:
编写了三个事件 onClick
、onDrag
、onPlaying
,分别对应了合成事件系统里三种优先级的事件,分别对触发对应的监听器。
其各个事件调用后的调用栈从上至下如下:
我们就着重研究每个调用栈的前半部分,弄明白 React 合成事件系统是如何运作的。
后半部分的 React 更新不在本文范围内,故不细讲。
监听器入口
根据调用栈以及上文的内容我们可以知道,当我们在页面点击按钮之后。以 onClick
为例子,率先触发的一定是挂载在根 DOM 节点上的 click 事件的监听器,也就是 dispatchDiscreteEvent
。
复习一下,在前文“注册时间代理-createEventListenerWrapperWithPriority”小节中提到,React 会根据不同的优先级提供不同的监听器,监听器共三种,分别是:
- 离散事件监听器:
dispatchDiscreteEvent
- 用户阻塞事件监听器:
dispatchUserBlockingUpdate
- 连续事件或其他事件监听器:
dispatchEvent
先简单说说这三个监听器的异同,
首先,这三个监听器的目的是相同的,最终目的都是进行事件收集、事件调用。具体代码层面,从调用栈可以知道,都会调用 dispatchEvent
(第三类监听器)这个函数。
但不同的是,监听器在调用 dispatchEvent
之前发生的事情不一样,连续事件或其他事件监听器(第三类监听器) 由于其优先级最高的原因所以是直接同步调用的,而另外两类不同。
所以我们现在需要明白的是,前两类监听器在调用 dispatchEvent
之前都做了什么,为什么这么做,再然后我们去着重关注一下 dispatchEvent
具体干了些啥就够了。
由于其内容复杂性的原因,前两个监听器,笔者会先讲第二类监听器“用户阻塞事件监听器”方便读者理解。
dispatchUserBlockingUpdate(用户阻塞事件监听器)
函数内容很简单,调用了一个函数 runWithPriority
,传入了 当前任务的优先级 和 想要执行的任务(函数)。runWithPriority
的将当前任务的优先级标记在全局静态变量中,方便内部的更新知道当前是在什么优先级的事件中执行的。
在此,也可以解释为何第三类监听器是直接调用 dispatchEvent
,而没有进行任何副作用操作,因为它是优先级坠高的,直接同步调用即可。
// 简化版本
// 前三个参数是在注册事件代理的时候便传入的,
// domEventName:对应原生事件名称
// eventSystemFlags:本文范文内其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
// container:应用根 DOM 节点
// nativeEvent:原生监听器传入的 Event 对象
function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
container,
nativeEvent,
),
)
}
dispatchDiscreteEvent(离散事件监听器)
第一类监听器相对于第二类监听就复杂点了,下述从上至下其部分调用链
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
// flushDiscreteUpdatesIfNeeded 的作用是清除先前积攒的为执行的离散任务,包括但不限于之前触发的离散事件 和 useEffect 的回调,
// 主要为了保证当前离散事件所对应的状态时最新的
// 如果觉得头疼,可以假装这行不存在,影响不大
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
// 新建一个离散更新
// 提前讲解一下它的入参,后面四个参数实际上第一个函数参数的参数
// 后面会这么调用,dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent),是不是很眼熟
discreteUpdates(
dispatchEvent,
domEventName,
eventSystemFlags,
container,
nativeEvent,
);
}
export function discreteUpdates(fn, a, b, c, d) {
// 标记当前正在事件处理过程中,并存储之前的状态
const prevIsInsideEventHandler = isInsideEventHandler;
isInsideEventHandler = true;
try {
// 当前函数只需要关注这一行就行了
// 调用 Scheduler 里的离散更新函数
return discreteUpdatesImpl(fn, a, b, c, d);
} finally {
// 如果之前就处于事件处理过程中,则继续完成
isInsideEventHandler = prevIsInsideEventHandler;
if (!isInsideEventHandler) {
finishEventHandler();
}
}
}
// react-reconciler 中的离散更新函数
// 会做两件事,第一件事是调用对应离散事件,第二件事更新离散事件中可能生成的更新(如果时机对)
// 如果你注意了前文离散事件监听器的调用栈,你会发现这里的两件事分别是
// 第一件事:合成事件系统对于真实事件的收集及真实事件的调用
// 第二件事:对真实事件中可能生成的更新进行更新
discreteUpdatesImpl = function discreteUpdates<A, B, C, D, R>(
fn: (A, B, C) => R,
a: A,
b: B,
c: C,
d: D,
): R {
// 添加当前执行上下文状态,用于判断当前处在什么情况下,比如 RenderContext 说明更新处于 render 阶段
// 全部上下文类型 https://github.com/facebook/react/blob/v17.0.0/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L249-L256
const prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
try {
// 这里就跟第二类监听就一模一样啦!
return runWithPriority(
UserBlockingSchedulerPriority,
fn.bind(null, a, b, c, d),
);
} finally {
// 回归之前上下文
executionContext = prevExecutionContext;
// 如果已经没有执行上下文,说明已经执行完了,则可以开始更新生成的更新
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
小结
dispatchEvent 之后
通过前文的描述,相信你一定清楚了 dispatchEvent
的重要性,它是所有花里胡哨的监听器的终点,其包含了合成事件系统的核心功能。
读者可以先回顾一下前文展示的调用栈,以 onClick
为例:
实际上 dispathEvent
中最核心的内容就是调用 dispatchEventsForPlugins
,因为正是这个函数触发了 事件收集、事件执行。
在此之前他会做一系列啰嗦的调度以及边缘情况的判断,对于主流程的参考价值不大,所以笔者打算跳过对这些内容的讲解,直奔主题,仅对其入参来源进行讲解。
dispatchEventsForPlugins
着重讲一下第四个入参 targetInst
,他的数据类型是 Fiber
或者 null
,通常它就是一个 Fiber
,所以大家可以忽略 null
的可能性,防止增加心智负担。
以前面测试 Demo 中的点击事件为例,targetInst
就是我们点击的那个 <button />
对应的 Fiber
节点。
那 React 是怎么获取到这个 Fiber
节点呢,发生地主要在 调用栈中出现过的 attemptToDispatchEvent
函数中,其中分两步走:
- 获取监听器中传进来的
Event
对象,并获取Event.target
内的 DOM 节点,这个 DOM 节点实际上就是<button />
。获取函数 - 获取存储在 DOM 节点上的
Fiber
节点,Fiber
节点实际上存在DOM.['__reactFiber$' + randomKey]
的键值上。获取函数,对应的赋值函数function dispatchEventsForPlugins(
domEventName: DOMEventName, // 事件名称
eventSystemFlags: EventSystemFlags, // 事件处理阶段,4 = 捕获阶段,0 = 冒泡阶段
nativeEvent: AnyNativeEvent, // 监听器的原生入参 Event 对象
targetInst: null | Fiber, // event.target 对应的 DOM 节点的 Fiber 节点
targetContainer: EventTarget, // 根 DOM 节点
): void {
// 这里也获取了一遍 event.target
const nativeEventTarget = getEventTarget(nativeEvent);
// 事件队列,收集到的事件都会存储到这
const dispatchQueue: DispatchQueue = [];
// 收集事件
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// 执行事件
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents(收集事件)
extractEvents
的内容其实很简单,按需调用几个 EventPlugin
的 extractEvents
,这几个 extractEvents
的目的是一样的,只不过针对不同的事件可能会生成不同的事件。我们就以最核心的也是最关键的 SimpleEventPlugin.extractEvents
来讲解
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
// 根据原生事件名称获取合成事件名称
// 效果: onClick = topLevelEventsToReactNames.get('click')
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
// 默认合成函数的构造函数
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
switch (domEventName) {
// 按照原生事件名称来获取对应的合成事件构造函数
}
// 是否是捕获阶段
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
if (
enableCreateEventHandleAPI &&
eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
) {
// ...和下文基本一致
} else {
// scroll 事件不冒泡
const accumulateTargetOnly =
!inCapturePhase && domEventName === 'scroll';
// 核心,获取当前阶段的所有事件
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
);
if (listeners.length > 0) {
// 生成合成事件的 Event 对象
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
// 入队
dispatchQueue.push({event, listeners});
}
}
}
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;
while (instance !== null) {
const {stateNode, tag} = instance;
// 如果是有效节点则获取其事件
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
if (reactEventName !== null) {
// 获取存储在 Fiber 节点上 Props 里的对应事件(如果存在)
const listener = getListener(instance, reactEventName);
if (listener != null) {
// 入队
listeners.push(
// 简单返回一个 {instance, listener, lastHostComponent} 对象
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
// scroll 不会冒泡,获取一次就结束了
if (accumulateTargetOnly) {
break;
}
// 其父级 Fiber 节点,向上递归
instance = instance.return;
}
// 返回监听器集合
return listeners;
}
processDispatchQueue(执行事件)
在分析源码之前,我们先整理一下 dispatchQueue
的数据类型,一般来说他的长度只会为 1。
interface dispatchQueue {
event: SyntheticEvent
listeners: {
instance: Fiber,
listener: Function,
currentTarget: Fiber['stateNode']
}[]
}[]
上源码
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
// 通过 eventSystemFlags 判断当前事件阶段
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 遍历合成事件
for (let i = 0; i < dispatchQueue.length; i++) {
// 取出其合成 Event 对象及事件集合
const {event, listeners} = dispatchQueue[i];
// 这个函数就负责事件的调用
// 如果是捕获阶段的事件则倒序调用,反之为正序调用,调用时会传入合成 Event 对象
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
// 抛出中间产生的错误
rethrowCaughtError();
}
总结
React 合成事件系统其实并不算特别复杂(相较于 Concurrent mode 的相关代码),其核心思想就是用事件代理统一接收事件的触发,然后由 React 自身来调度真实事件的调用,而 React 如何知道应该从哪开始收集事件的核心其实是存储在真实 DOM 上的 Fiber 节点,从真实穿梭到虚拟。
其他
相较于 16.x 版本的区别
提三个个人感知比较强烈的变动
1. 监听器挂载的节点由 Document 节点改为 RootNode(根 DOM 节点)
相关 PR:Modern Event System: add plugin handling and forked paths #18195
其主要作用是收束合成事件系统的影响范围,假设我一个 Web 应用中有一个 React 应用的同时还有其他应用也用到了 Document 的事件监听,这个时候很大概率会互相影响导致一方的错误,但当我们将合成事件系统收束到当前 React 应用的根 DOM 节点的时候这种副作用就会减少很多了。
2. 当挂载 root 时,添加所有已知的事件监听器,而不是在 completeWork 阶段按需添加监听器
相关 PR:Attach Listeners Eagerly to Roots and Portal Containers #19659
主要作用是修复了 createPortal
导致事件冒泡的问题,虽然问题不大,但对应源码的变动是真多。
3. 移除事件池
相关 PR:
原先是为了提高性能而存在的设计,但导致事件中异步更新的一些心智负担,所以就在没有太大性能影响的现在移除了。
Vue 有自己的事件机制吗,跟 React 有什么差异?
Vue 其实在事件上也做了很多自己的内容,主要是为了方便开发者的开发,比如各种修饰符或者是各种指令,其核心运作的方式还是依靠其模板编译来实现的,是一个更倾向于编译时的特性,绑定事件也是在其 patch
阶段一个个将事件挂载在对应 DOM 上,而非使用事件代理统一分发。
参考
- UI Events | W3C Working Draft, 04 August 2016
- Event - Web API 接口参考 | MDN
- React v17.0 RC 发版声明
- Modern Event System: add plugin handling and forked paths #18195
- Attach Listeners Eagerly to Roots and Portal Containers #19659
- Remove event pooling in the modern system #18216
- Remove event pooling in the modern system #18969
- 深入React合成事件机制原理
- 谈谈React事件机制和未来(react-events)