基于原生 over/out 事件计算鼠标 / 指针进出关系,屏蔽原生冒泡缺陷、抹平浏览器与 SSR / 并发渲染差异,生成语义化非冒泡 onMouseEnter/onMouseLeave/onPointerEnter/onPointerLeave 合成事件,配套专用 Fiber 遍历、回调收集逻辑,支撑页面 hover 悬浮、移入移出交互,大幅降低拖拽、绘图、hover 悬浮等高频率场景计算开销
注册事件:registerEvents
浏览器原生 mouseenter/mouseleave、pointerenter/pointerleave 不冒泡 ,无法用 React 根容器事件委托实现,且原生事件不会跨 Portal、跨根容器正确隔离。React 不直接监听 enter/leave,转而监听会冒泡的 mouseover/mouseout / pointerover/pointerout,在插件内部手动模拟 enter/leave 语义
- 模块初始化一次性执行,运行时无开销:仅在框架加载阶段执行一次,预注册全部元数据,事件触发时直接读取映射,不重复计算
- 注册模拟型合成事件,建立事件依赖关系 :对外提供
onMouseEnter/onMouseLeave/onPointerEnter/onPointerLeave API,声明它们依靠over/out冒泡原生事件实现 - 填充全局原生事件池
allNativeEvents:让根容器初始化时自动绑定mouseover/mouseout/pointerover/pointerout,支撑事件委托模拟enter/leave
javascript
function registerEvents() {
// 模拟 onMouseEnter / onMouseLeave,依赖原生 mouseover、mouseout
registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
// 模拟 onPointerEnter / onPointerLeave,依赖原生 pointerover、pointerout
registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}
提取事件:extractEvents
- 原生
over/out映射为非冒泡enter/leave合成事件 :原生mouseover/mouseout会逐层冒泡,业务很难实现元素级单独监听;本函数基于relatedTarget计算进出元素,生成不冒泡 的onMouseEnter/onMouseLeave/onPointerEnter/onPointerLeave,收集对应层级回调推入调度队列 - 完整处理鼠标 / 触屏移入移出全边界场景
- 页面外部 → 组件(仅
enter) - 组件 → 页面外部(仅
leave) - A 组件 → B 组件(同时触发
A leave、B enter) - 组件内部微小移动(无事件)
- 页面外部 → 组件(仅
javascript
function extractEvents(
dispatchQueue,
// 原生事件名 mouseover/mouseout/pointerover/pointerout
domEventName,
// 触发事件DOM对应的Fiber
targetInst,
// 原生DOM Event对象
nativeEvent,
// 原生触发DOM
nativeEventTarget,
eventSystemFlags,
// React根容器DOM
targetContainer,
) {
const isOverEvent =
domEventName === 'mouseover' || domEventName === 'pointerover';
const isOutEvent =
domEventName === 'mouseout' || domEventName === 'pointerout';
if (isOverEvent && !isReplayingEvent(nativeEvent)) {
const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
if (related) {
if (
getClosestInstanceFromNode(related) ||
isContainerMarkedAsRoot(related)
) {
return;
}
}
}
if (!isOutEvent && !isOverEvent) {
return;
}
let win;
if (nativeEventTarget.window === nativeEventTarget) {
win = nativeEventTarget;
} else {
const doc = nativeEventTarget.ownerDocument;
if (doc) {
win = doc.defaultView || doc.parentWindow;
} else {
win = window;
}
}
let from;
let to;
if (isOutEvent) {
const related = nativeEvent.relatedTarget || nativeEvent.toElement;
from = targetInst;
to = related ? getClosestInstanceFromNode(related) : null;
if (to !== null) {
const nearestMounted = getNearestMountedFiber(to);
const tag = to.tag;
if (
to !== nearestMounted ||
(tag !== HostComponent && tag !== HostSingleton && tag !== HostText)
) {
to = null;
}
}
} else {
from = null;
to = targetInst;
}
if (from === to) {
return;
}
let SyntheticEventCtor = SyntheticMouseEvent;
let leaveEventType = 'onMouseLeave';
let enterEventType = 'onMouseEnter';
let eventTypePrefix = 'mouse';
if (domEventName === 'pointerout' || domEventName === 'pointerover') {
SyntheticEventCtor = SyntheticPointerEvent;
leaveEventType = 'onPointerLeave';
enterEventType = 'onPointerEnter';
eventTypePrefix = 'pointer';
}
const fromNode = from == null ? win : getNodeFromInstance(from);
const toNode = to == null ? win : getNodeFromInstance(to);
const leave = new SyntheticEventCtor(
leaveEventType,
eventTypePrefix + 'leave',
from,
nativeEvent,
nativeEventTarget,
);
leave.target = fromNode;
leave.relatedTarget = toNode;
let enter = null;
const nativeTargetInst = getClosestInstanceFromNode(nativeEventTarget);
if (nativeTargetInst === targetInst) {
const enterEvent = new SyntheticEventCtor(
enterEventType,
eventTypePrefix + 'enter',
to,
nativeEvent,
nativeEventTarget,
);
enterEvent.target = toNode;
enterEvent.relatedTarget = fromNode;
enter = enterEvent;
}
accumulateEnterLeaveTwoPhaseListeners(dispatchQueue, leave, enter, from, to);
}
作用
- 标记当前是否为
over /out原生事件 over事件前置过滤:仅当鼠标从页面空白或外部组件移入组件 时才生成enter事件;内部组件互相移动不触发多余enter,避免重复执行- 取出
relatedTarget(鼠标来源元素) - 如果来源元素属于
React渲染内部DOM(存在对应Fiber/ 是根容器),直接终止函数
- 取出
- 非
over/out事件直接退出本分支 - 获取全局
window对象,用于页面外移入 / 移出边界兜底:兼容多窗口、iframe场景,统一拿到当前页面window;当鼠标从页面外移入 / 移出时,relatedTarget为null,使用window作为虚拟target - 计算
from(离开元素Fiber)、to(进入元素Fiber)- 移出事件
mouseout/pointeroutfrom = targetInst:当前鼠标离开的元素Fiberto:通过relatedTarget找到鼠标进入的元素对应Fiber,必须是已挂载的Fiber且必须是真实DOM/ 文本节点(HostComponent/HostSingleton/HostText),不满足则置为null,视为移出页面
- 移入事件
mouseover/pointeroverfrom = null(从页面外部进来)to = targetInst(当前进入的元素Fiber)
- 移出事件
- 离开、进入为同一个元素,无需触发事件,直接返回
- 根据原生事件区分鼠标 / 指针两套合成事件构造器与事件名
- 鼠标事件:使用
SyntheticMouseEvent,事件名onMouseLeave/onMouseEnter - 触屏 / 手写笔指针事件:使用
SyntheticPointerEvent,事件名onPointerLeave/onPointerEnter
- 鼠标事件:使用
- 生成合成事件
target、relatedTarget真实DOMfrom为null(从页面移入):target使用windowto为null(移出页面):relatedTarget使用window
- 构造
leave合成事件(onMouseLeave /onPointerLeave):target= 离开的DOM,relatedTarget= 进入的DOM,代表鼠标离开from元素 - 条件构造
enter合成事件(onMouseEnter /onPointerEnter):仅当原生事件触发节点与计算出的to完全匹配时,才创建enter事件,enter事件target为进入的DOM,relatedTarget为离开的DOM - 调用专用收集函数
accumulateEnterLeaveTwoPhaseListeners,收集移入、移出回调并入队
设计意义
解决原生 mouseover 冒泡冗余触发问题 :原生 mouseover 会层层冒泡,父、子元素都会触发 over 事件,当来源是 React 内部 DOM,直接放弃生成 enter 事件
辅助函数
移入移出两阶段事件回调收集:accumulateEnterLeaveTwoPhaseListeners
专门处理 mouseover/mouseout / pointerover/pointerout 这类带 relatedTarget 的移入移出事件
- 调用
getLowestCommonAncestor以getParent(取Fiber.return)为遍历规则,计算新旧元素最低公共祖先common。若from为空(鼠标从页面外移入)或to为空(鼠标移出页面),common = null,遍历无边界限制,一路向上到根节点 - 处理移出阶段
mouseout:从离开元素向上走到公共祖先,收集所有onMouseOut回调存入队列直到公共祖先停止,无离开元素(页面外移入)直接跳过 - 处理移入阶段
mouseover:从进入元素向上走到公共祖先,收集所有onMouseOver回调存入队列直到公共祖先停止,无目标元素 / 无移入事件模板直接跳过
javascript
export function accumulateEnterLeaveTwoPhaseListeners(
dispatchQueue,
// 移出侧合成事件模板(mouseout / pointerout)
leaveEvent,
// 移入侧合成事件模板(mouseover / pointerover)
enterEvent,
// relatedTarget,鼠标离开的 DOM 对应 Fiber
from,
// 当前触发事件的 DOM 对应 Fiber
to,
) {
// from 和 to 的最低公共祖先 Fiber,作为遍历终止边界
const common =
from && to ? getLowestCommonAncestor(from, to, getParent) : null;
if (from !== null) {
// 单方向收集移入 / 移出回调
accumulateEnterLeaveListenersForEvent(
dispatchQueue,
leaveEvent,
from,
common,
false,
);
}
if (to !== null && enterEvent !== null) {
accumulateEnterLeaveListenersForEvent(
dispatchQueue,
enterEvent,
to,
common,
true,
);
}
}
设计意义
鼠标从 A 元素移动到 B 元素时,两者公共祖先以上的组件不会同时触发移入 + 移出,只需遍历到公共祖先就停止,划定移入、移出遍历的终止边界,防止公共祖先上层组件重复触发事件,逻辑错乱,同时大幅减少 Fiber 遍历次数,优化性能,提升 mousemove 高频场景性能,大幅降低连续 mousemove 拖拽、绘图场景的计算开销
移入/移出单条链路的回调收集:accumulateEnterLeaveListenersForEvent
专门处理 mouseover/mouseout / pointerover/pointerout 移入 / 移出单条链路的回调收集
- 从起点
target向上遍历Fiber,遇到公共祖先common立刻终止循环 - 捕获阶段从前到后插入数组(
unshift),冒泡阶段顺序追加(push),对齐W3C捕获先执行、冒泡后执行的顺序 - 只收集真实
DOM宿主组件(HostComponent)上绑定的移入移出事件,过滤无效组件,减少props读取、循环判断开销,面向高频滑动场景做性能优化
javascript
function accumulateEnterLeaveListenersForEvent(
dispatchQueue,
event,
target,
common,
inCapturePhase,
) {
const registrationName = event._reactName;
const listeners = [];
let instance = target;
while (instance !== null) {
if (instance === common) {
break;
}
const {alternate, stateNode, tag} = instance;
if (alternate !== null && alternate === common) {
break;
}
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
stateNode !== null
) {
const currentTarget = stateNode;
if (inCapturePhase) {
const captureListener = getListener(instance, registrationName);
if (captureListener != null) {
listeners.unshift(
createDispatchListener(instance, captureListener, currentTarget),
);
}
} else if (!inCapturePhase) {
const bubbleListener = getListener(instance, registrationName);
if (bubbleListener != null) {
listeners.push(
createDispatchListener(instance, bubbleListener, currentTarget),
);
}
}
}
instance = instance.return;
}
if (listeners.length !== 0) {
dispatchQueue.push({event, listeners});
}
}
作用
- 提取事件注册名、初始化存储数组:取出事件对应的
JSX注册名,初始化回调数组(存放当前链路、当前阶段收集到的标准化回调对象) Fiber向上遍历循环,起点为target:从传入的起点Fiber(离开元素 / 进入元素)逐层向上遍历父节点instance.return- 命中公共祖先直接跳出循环(性能核心优化):只遍历新旧元素之间的层级,公共祖先及上层不再处理,限制遍历范围,避免整棵组件树循环,大幅减少循环次数,优化高频
mousemove- 当前遍历到的
Fiber等于公共祖先common,停止遍历 - 兼容双
Fiber树(current/alternate),若当前节点的备用树是公共祖先,同样终止
- 当前遍历到的
- 仅处理真实
DOM宿主组件(HostComponent系列):currentTarget缓存当前DOM,用于封装调度监听对象- 捕获阶段逻辑
- 调用
getListener从Fiber.props读取捕获事件回调onMouseOverCapture unshift头部插入:上层祖先捕获回调先执行,符合捕获阶段从外到内执行顺序
- 调用
- 冒泡阶段逻辑
- 调用
getListener从Fiber.props读取普通冒泡事件onMouseOver push尾部追加:从内到外执行,符合冒泡传播顺序
- 调用
- 捕获阶段逻辑
- 指针上移,遍历父
Fiber,循环向上遍历祖先节点 - 存在回调则推入全局调度队列
查找 DOM 对应的 Fiber:getClosestInstanceFromNode
输入一个原生 DOM 节点,向上逐层查找距离该节点最近、由 React 管理的 Fiber 实例
- 尝试读取当前节点自身绑定的
Fiber,如果当前DOM本身就是HostComponent,直接返回对应Fiber- 现代模式:从全局
WeakMap根据DOM取Fiber - 旧模式:读取
DOM上挂载的私有internalInstanceKey Symbol
- 现代模式:从全局
- 初始化父节点遍历指针,向上循环:当前节点无
Fiber,逐层向上遍历父DOM,直到parentNode = null(到文档根) - 读取父
DOM上存储的Fiber(优先容器实例,再普通实例) - 找到有效
Fiber,进入hydration边界特殊判断:当前Fiber存在子节点(current树或alternate备用树有子节点),说明是容器类Fiber(div、根容器等) ,需要走水合边界修正逻辑 - 向上遍历水合边界,查找真实挂载
Fiber:解决SSR水合阶段DOM与Fiber匹配错位问题- 调用
getParentHydrationBoundary获取服务端渲染水合边界父DOM - 容器
DOM场景,不直接返回当前容器Fiber,而是向上找到水合边界对应的真实Fiber
- 调用
- 非水合容器场景,直接返回找到的父
Fiber - 指针上移,继续向上遍历父
DOM - 整棵
DOM树遍历完毕,无任何React管理节点:DOM完全不在React渲染树内(外部iframe、第三方DOM、空白body),返回null
javascript
export function getClosestInstanceFromNode(targetNode) {
let targetInst;
if (enableInternalInstanceMap) {
// 使用 WeakMap(internalInstanceMap) 存储 DOM → Fiber(现代模式,不污染 DOM 属性)
targetInst = internalInstanceMap.get(targetNode);
} else {
// 直接在 DOM 对象挂载内部 Symbol 键(旧模式)
targetInst = targetNode[internalInstanceKey];
}
if (targetInst) {
return targetInst;
}
let parentNode = targetNode.parentNode;
while (parentNode) {
if (enableInternalInstanceMap) {
// internalContainerInstanceKey:React 根容器 / Portal 容器标记
targetInst =
parentNode[internalContainerInstanceKey] ||
internalInstanceMap.get(parentNode);
} else {
targetInst =
parentNode[internalContainerInstanceKey] ||
parentNode[internalInstanceKey];
}
if (targetInst) {
const alternate = targetInst.alternate;
if (
targetInst.child !== null ||
(alternate !== null && alternate.child !== null)
) {
let hydrationInstance = getParentHydrationBoundary(targetNode);
while (hydrationInstance !== null) {
const targetFiber = enableInternalInstanceMap
? internalInstanceMap.get(hydrationInstance)
: hydrationInstance[internalInstanceKey];
if (targetFiber) {
return targetFiber;
}
hydrationInstance = getParentHydrationBoundary(hydrationInstance);
}
}
return targetInst;
}
targetNode = parentNode;
parentNode = targetNode.parentNode;
}
return null;
}
定位 SSR 水合区块顶层边界:getParentHydrationBoundary
SSR 水合注释标记机制 :React SSR 服务端输出 HTML 时,会插入特殊 HTML 注释节点作为分割标记(水合边界标记 ),用来识别 Suspense/Activity 边界
<!--$-->/<!--$!-->/<!--$?-->/<!--$~-->:Suspense开始标记<!--/$-->:Suspense结束标记- / :`Activity` 配套起止注释
向左遍历同层前置兄弟注释,匹配 Suspense/Activity 边界专属注释标记,找到当前节点所属的水合区块起始注释节点;用于修复水合阶段 DOM ↔ Fiber 映射错位问题。天然区分 CSR / SSR 环境,CSR 页面无任何 SSR 注释,遍历后直接返回 null,仅 SSR 页面、水合阶段才会返回有效边界注释
javascript
export function getParentHydrationBoundary(
targetInstance,
) {
// 从当前节点的前一个兄弟节点开始向左遍历
let node = targetInstance.previousSibling;
while (node) {
if (node.nodeType === COMMENT_NODE) {
const data = node.data;
// 匹配各类 Suspense / Activity 起始注释
if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA
) {
// 找到顶层边界(depth归零),直接返回该注释节点作为水合边界
if (depth === 0) {
return node;
} else {
// 内层嵌套边界,层级-1,继续往左找外层边界
depth--;
}
} else if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
// 遇到结束注释,说明进入嵌套区块,层级+1
depth++;
}
}
// 继续向左遍历上一个兄弟
node = node.previousSibling;
}
// 同层没有找到任何水合起始注释,返回null
return null;
}
查找完成挂载的最近祖先 Fiber:getNearestMountedFiber
输入任意 Fiber,向上查找最近一个已经完成挂载、稳定可用的祖先 Fiber ;若最终根节点不是 HostRoot(脱离 React 树),返回 null
- 过滤临时未提交
Fiber:仅无alternate的临时节点进入循环,持续向上追溯,跳过刚创建、未渲染完成的节点 - 识别
Placement/Hydrating未稳定节点 :标记存在代表DOM还未完全挂载 / 水合,不能作为relatedTarget事件目标,记录其父节点作为稳定替代 - 追溯到全局根节点做合法性校验 :防止处理已经卸载、脱离
React根树的游离Fiber- 如果顶层根是
HostRoot(标准React应用根),返回之前缓存的最近稳定挂载祖先nearestMounted - 如果根不是
HostRoot(节点已卸载、脱离渲染树、Portal隔离等),直接返回null,代表无有效挂载节点
- 如果顶层根是
javascript
export function getNearestMountedFiber(fiber) {
let node = fiber;
let nearestMounted;
let nextNode = node;
while (nextNode && !nextNode.alternate) {
node = nextNode;
// 判断当前节点是否处于新增/水合未完成状态
if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
// 当前节点未挂载,它的父节点是候选稳定节点
nearestMounted = node.return;
}
// 向上走父节点
nextNode = node.return;
}
while (node.return) {
node = node.return;
}
if (node.tag === HostRoot) {
return nearestMounted;
}
return null;
}