各位同学好,欢迎来到今天的“React 源码深度解析”现场。我是你们的讲师。
今天我们不讲 useEffect 的依赖陷阱,也不讲 Context 的性能大坑。我们要聊的是 React 的“神经系统”——事件系统。
尤其是当你在屏幕上疯狂点击按钮,React 是如何在毫秒级的时间内,把一个原生的 DOM 事件“包装”成一个 React 事件,并把它塞进那个复杂的 Lane(车道)调度系统里的?这简直就像是一场精心编排的特工行动。
准备好了吗?系好安全带,我们要钻进 React 的核心代码库了。
第一部分:伪装的艺术——原生事件是如何变成 SyntheticEvent 的?
在 React 出现之前,我们直接监听 DOM 事件。但在 React 出现之后,事情变得有点复杂。React 告诉你:“别去管 onclick,那是个假象。”
React 的策略是:监听原生事件,包装成合成事件,冒泡机制照旧。
1. 拦截与伪装
React 不会给每一个 button 挂载一个 addEventListener。那样太重了,性能太差。相反,React 在根节点(通常是 document 或挂载点)上只挂载了几个监听器。这些监听器监听的是什么呢?是那些浏览器原生的、高频触发的事件,比如 mousedown、mouseup、click、focus、input 等等。
React 把这些原生事件称为 topLevelTypes。这就像是一个特工接头暗号,比如 topClick 对应原生的 click。
让我们看看源码里是怎么做的(简化版):
// packages/react-dom/events/ReactDOMEventListener.js
class ReactDOMEventListener {
// ...
// 初始化监听器
listenTo(topLevelType, element) {
const listenerMap = this.eventPluginRegistry.getListenerMap();
// 获取对应的事件插件(比如 SimpleEventPlugin)
const eventType = listenerMap[topLevelType];
// 在 document 上注册原生监听器
element.addEventListener(
eventType,
this.handleTopLevel, // 核心分发器
false
);
}
// 核心分发器:这是所有事件的“中转站”
handleTopLevel(event) {
// 1. 获取原生事件对象
const nativeEvent = event;
// 2. 提取顶层类型(比如 'click')
const eventSystemFlags = 0; // 标志位
const targetInst = getClosestInstanceFromNode(nativeEvent.target);
const topLevelType = getTopLevelEventType(nativeEvent.type);
// 3. 开始分发:这是我们要讲的核心!
this.dispatchEvent(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst
);
}
// 最终分发:这里开始介入优先级系统
dispatchEvent(topLevelType, eventSystemFlags, nativeEvent, targetInst) {
// 检查事件冒泡是否被阻断
if (!ReactErrorUtils.invokeGuardedCallback(...)) {
return;
}
// 获取事件优先级(Lane)!
const eventPriority = getEventPriority(topLevelType);
// 调度执行:这是关键步骤
// 我们要把这个事件,按照它的优先级,放入调度队列
Scheduler_runWithPriority(eventPriority, () => {
this.executeDispatchesInOrder(nativeEvent, targetInst);
});
}
}
看到了吗?当你在点击按钮时,实际上是在 document 上发生了一次 mousedown。React 的 ReactDOMEventListener 捕获了这个原生事件,然后调用 dispatchEvent。
2. SyntheticEvent 的诞生
在 dispatchEvent 之前,React 需要把那个脏兮兮的原生 DOM 事件对象,清洗成一个干净、跨浏览器一致的 SyntheticEvent。
这就像是把一个满身泥泞的村口二狗子,洗刷干净,穿上西装,变成了华尔街精英。这个 SyntheticEvent 封装了 nativeEvent,并提供了 preventDefault、stopPropagation 等方法。
代码大概长这样(简化):
// packages/react-dom/events/SyntheticEvent.js
class SyntheticEvent {
constructor(dispatchConfig, targetInst, nativeEvent) {
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;
// ... 封装一堆方法
this.isDefaultPrevented = () => this.nativeEvent.defaultPrevented;
this.stopPropagation = () => {
this.nativeEvent.stopPropagation();
this.isPropagationStopped = () => true;
};
}
}
第二部分:车道优先级——为什么我们需要 Lane?
好,事件已经被包装好了。现在,React 面临着一个巨大的哲学问题:这个事件重要吗?它该排在队首,还是排队吃盒饭?
在 Concurrent Mode(并发模式)之前,React 使用的是简单的 priority 概念:ImmediatePriority(最高),UserBlockingPriority,NormalPriority。这就像高速公路只有三条车道:快车道、慢车道、普通车道。
但在并发模式下,React 需要处理几十种优先级。为什么?因为我们需要在渲染过程中随时中断低优先级的更新(比如统计上报),去响应高优先级的更新(比如用户点击了“提交”按钮)。
于是,React 引入了 Lane(车道)。这就像是一个拥有 2^29 条车道的高速公路。每条车道代表一个优先级。
Lane 的概念
Lane 本质上是一个整数(位掩码)。低数值代表高优先级(比如 Lane 1 是紧急车道),高数值代表低优先级(比如 Lane 30 是慢车道)。
React 定义了一组常量来表示事件类型对应的优先级:
// packages/react-reconciler/lanes.js
// 我们关心的是事件优先级,主要是这些:
const DiscreteEventPriority = 1; // 离散事件,比如 click, keydown
const ContinuousEventPriority = 2; // 连续事件,比如 mousemove, scroll
const DefaultEventPriority = 4; // 默认事件
const IdleEventPriority = 1073741824; // 空闲事件
第三部分:翻译官——原生事件如何映射到 Lane?
这是最精彩的部分。React 怎么知道 click 是高优先级,而 scroll 是低优先级?
它有一个 getEventPriority 函数。这个函数就像是一个翻译官,拿着一份“事件字典”,把原生事件类型翻译成 Lane 优先级。
让我们看看这个翻译官的代码逻辑(极度简化):
// packages/react-dom/events/getEventPriority.js
function getEventPriority(topLevelType) {
switch (topLevelType) {
case 'click':
case 'focusin':
case 'focusout':
case 'keydown':
case 'keyup':
case 'submit':
case 'change':
// 所有的离散事件,都映射到 Lane 1 (DiscreteEventPriority)
return DiscreteEventPriority;
case 'pointerdown':
case 'pointermove':
case 'pointerup':
case 'pointerleave':
// 指针事件映射到 Lane 2 (ContinuousEventPriority)
return ContinuousEventPriority;
case 'scroll':
case 'touchstart':
case 'touchmove':
case 'touchend':
case 'touchcancel':
// 滚动事件映射到 Lane 4 (DefaultEventPriority)
return DefaultEventPriority;
// ... 还有很多其他事件
}
}
核心逻辑:
当 dispatchEvent 被调用时,React 首先调用 getEventPriority(topLevelType)。
- 如果是
click,返回1。 - 如果是
mousemove,返回2。 - 如果是
scroll,返回4。
这个数字(Lane)就是事件的“身份证号”,它决定了这个事件在调度队列中的位置。
第四部分:调度与执行——把事件塞进渲染队列
现在,我们有了 SyntheticEvent,也有了对应的 Lane(比如 Lane 1)。接下来,React 需要把这个事件“提交”给渲染器。
React 使用了 Scheduler 库来管理这些优先级。Scheduler 是 React 的心脏,它负责决定什么时候渲染、什么时候暂停。
1. runWithPriority
还记得之前的 dispatchEvent 吗?里面有一行代码:
Scheduler_runWithPriority(eventPriority, () => {
this.executeDispatchesInOrder(nativeEvent, targetInst);
});
这行代码是神来之笔。它告诉调度器:“嘿,我现在要执行一段代码,这段代码的优先级是 eventPriority(Lane 1)。”
Scheduler_runWithPriority 会根据传入的优先级,调整当前的调度状态。如果当前正在执行一个低优先级的任务(比如正在渲染一个耗时的列表),React 会立即中断它,把 CPU 切换到高优先级任务上来处理这个点击事件。
2. invokeGuardedCallback
在执行用户定义的回调函数(比如你的 onClick={handleClick})之前,React 还有一层保护。这层保护叫 invokeGuardedCallback。
为什么需要“Guarded”?因为用户的代码可能会报错。如果用户在 handleClick 里写了一个 throw new Error,React 不希望整个 React 应用崩溃,或者导致后续的事件监听器失效。
// 简化版逻辑
function invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {
let hasError = false;
let error = null;
try {
// 执行用户函数
func.call(context, a, b, c, d, e, f);
} catch (err) {
hasError = true;
error = err;
}
if (hasError) {
// 如果报错,处理错误
ReactErrorUtils.invokeGuardedCallbackDev(...);
return;
}
}
3. executeDispatch
最后,用户定义的函数被执行了。这个函数会修改 React 的 Fiber 树(比如通过 useState 更新状态,或者通过 useEffect 触发副作用)。
// packages/react-dom/events/ReactDOMFiberComponent.js
function executeDispatch(event, dispatchConfig, listener, domEventTarget) {
// 获取事件实例
const eventInterface = getEventInterface(dispatchConfig);
const nativeEvent = event.nativeEvent;
// 创建 SyntheticEvent 实例
const syntheticEvent = new SyntheticEvent(
dispatchConfig,
targetInst,
nativeEvent,
domEventTarget
);
// 调用用户注册的 listener
// 比如:onClick={handleClick} -> handleClick(syntheticEvent)
listener.call(syntheticEvent, syntheticEvent);
}
第五部分:批处理——React 的魔法手
你有没有遇到过这种情况:在 onClick 里调用了两个 setState,结果只执行了一次更新?
这就是 React 的 Batching(批处理) 机制。React 会把同一帧内的多个状态更新合并成一次渲染。
批处理是如何与事件优先级协同工作的呢?
1. 普通事件中的批处理
在 Scheduler_runWithPriority 的回调中,React 会开启一个批处理上下文。如果你在 onClick 里连续调用 setState,React 会把它们打包,等到调度器指示“可以渲染”的时候,一次性渲染。
2. 并发模式下的复杂批处理
在并发模式下,批处理变得非常智能。
假设你正在渲染一个低优先级的列表(Lane 30),此时用户点击了一个按钮(Lane 1)。React 会立即中断低优先级渲染,去处理高优先级点击事件。
关键点来了: 如果你在点击事件的回调里又触发了一个低优先级的状态更新,React 依然会把这些更新批处理在一起。
React 内部维护了一个 isBatchingUpdates 标志位。在事件处理函数执行期间,这个标志位是 true。这意味着所有的状态更新都会被暂存,而不是立即触发渲染。
// packages/react-dom/events/ReactDOMEventReplaying.js
// 简化逻辑
function batchedUpdates(fn, a, b) {
// 如果当前正在批处理,直接调用
if (isBatchingUpdates) {
return fn(a, b);
}
// 否则,开启批处理上下文
return batchedUpdatesImpl(fn, a, b);
}
第六部分:Lane 与渲染周期的纠缠——渲染何时发生?
现在,我们的点击事件已经执行完了,状态已经更新了。但是,React 什么时候真正去渲染这个更新呢?
这就是 Lane 的真正威力所在。
1. renderLanes
在每一帧渲染开始时,React 会定义一个 renderLanes。这代表“当前帧需要渲染哪些任务”。
2. 事件优先级 vs 渲染优先级
当点击事件发生时,它会被分配一个 lane(比如 lane = 1)。React 会把这个 lane 加入到调度队列中。
当调度器决定“该干活了”时,它会检查调度队列,看看有哪些 lane 需要处理。
- 如果
renderLanes包含lane 1(点击事件的优先级),React 会立即渲染这个更新。 - 如果
renderLanes只包含lane 30(低优先级动画),而点击事件是lane 1,React 会抢占渲染权,先渲染点击事件,然后再继续处理动画。
3. requestPaint
为了确保高优先级的事件不被阻塞,React 会调用 requestPaint。这会告诉浏览器:“嘿,如果可能的话,在当前帧的剩余时间里,给用户展示一下最新的画面。”
第七部分:实战演练——手写一个微型 React 事件分发器
为了让大家彻底理解,我们来手写一个迷你版的 React 事件分发器,包含 Lane 映射。
// --- 1. 定义 Lane 优先级 ---
const Lanes = {
Discrete: 1, // 离散事件 (Click, Keydown)
Continuous: 2, // 连续事件 (Mousemove)
Idle: 2147483648, // 空闲事件
};
// --- 2. 定义事件映射 ---
const EventMap = {
click: Lanes.Discrete,
input: Lanes.Discrete,
mousemove: Lanes.Continuous,
scroll: Lanes.Idle,
};
// --- 3. 模拟调度器 ---
function Scheduler_runWithPriority(lane, callback) {
console.log(`🚀 [调度器] 接收到优先级为 Lane ${lane} 的任务,开始执行...`);
callback();
console.log(`✅ [调度器] 任务执行完毕`);
}
// --- 4. 模拟 React 事件分发器 ---
class MiniReact {
constructor() {
// 假设我们在 document 上监听了所有原生事件
document.addEventListener('click', (e) => this.handleTopLevel(e));
document.addEventListener('mousemove', (e) => this.handleTopLevel(e));
document.addEventListener('scroll', (e) => this.handleTopLevel(e));
}
handleTopLevel(nativeEvent) {
// 1. 识别原生事件类型
const topLevelType = nativeEvent.type;
// 2. 获取事件优先级
const eventPriority = EventMap[topLevelType] || Lanes.Idle;
console.log(`🔍 [分发器] 检测到原生事件: ${topLevelType}, 优先级: Lane ${eventPriority}`);
// 3. 包装合成事件
const syntheticEvent = {
type: topLevelType,
target: nativeEvent.target,
preventDefault: () => console.log(`🛡️ [合成事件] 阻止了默认行为`),
stopPropagation: () => console.log(`🚫 [合成事件] 阻止了冒泡`),
};
// 4. 调度执行 (这是核心!)
Scheduler_runWithPriority(eventPriority, () => {
this.executeDispatches(nativeEvent, syntheticEvent);
});
}
executeDispatches(nativeEvent, syntheticEvent) {
// 模拟冒泡查找
let current = nativeEvent.target;
while (current) {
// 模拟找到 React Fiber 节点
const fiberNode = this.findFiber(current);
if (fiberNode) {
// 找到注册的事件监听器
const listener = fiberNode.props[`on${syntheticEvent.type}`];
if (listener) {
console.log(`⚡ [执行] 在 Fiber 节点 ${current.id} 上触发回调`);
// 调用用户回调
listener(syntheticEvent);
// 如果阻止了冒泡,停止查找
if (syntheticEvent.isPropagationStopped) {
break;
}
}
}
current = current.parentNode;
}
}
findFiber(domNode) {
// 简单模拟:返回一个对象
return { id: domNode.id, props: domNode.props };
}
}
// --- 5. 使用 ---
const button = document.createElement('button');
button.id = 'btn-1';
button.textContent = '点击我';
button.onclick = () => {
console.log('🔥 用户点击了按钮!');
// 模拟连续点击
};
document.body.appendChild(button);
const app = new MiniReact();
运行结果预览:
当你点击按钮时,你会看到:
🔍 [分发器] 检测到原生事件: click, 优先级: Lane 1🚀 [调度器] 接收到优先级为 Lane 1 的任务,开始执行...⚡ [执行] 在 Fiber 节点 btn-1 上触发回调🔥 用户点击了按钮!
当你移动鼠标时,你会看到:
🔍 [分发器] 检测到原生事件: mousemove, 优先级: Lane 2🚀 [调度器] 接收到优先级为 Lane 2 的任务,开始执行...
这个微缩模型完美还原了 React 的逻辑:识别事件 -> 获取优先级 -> 调度 -> 执行。
第八部分:深入细节——事件复用与内存优化
你可能会问:SyntheticEvent 对象是每次都 new 的新对象吗?如果点击速度很快,会不会内存溢出?
当然不会。React 是一个极其抠门(褒义)的工程师。
React 内部维护了一个 Event Pool(事件池)。
当 SyntheticEvent 被创建时,它并不是从堆内存里 new 出来的。相反,它从池子里拿。当事件被使用完毕后,React 会把它的属性重置,然后放回池子里供下一次使用。
// 简化的 Event Pool 逻辑
const eventPool = [];
function getPooledEvent(dispatchConfig, targetInst) {
const event = eventPool.pop() || new SyntheticEvent();
event.init(dispatchConfig, targetInst);
return event;
}
function releasePooledEvent(event) {
event.isPropagationStopped = false;
event.isDefaultPrevented = false;
// 重置其他属性...
eventPool.push(event);
}
// 在 executeDispatch 中
function executeDispatch(event, listener) {
// 使用 event
listener(event);
// 使用完毕,归还
releasePooledEvent(event);
}
这就是为什么你在事件处理函数里 console.log(event),你会发现每次打印的对象引用都是同一个(或者结构相同),但时间戳不同。这是 React 为了性能做出的巨大牺牲。
第九部分:总结与回顾
好了,各位同学,让我们把镜头拉远,看看整个流程的全景图。
- 拦截: React 在
document上监听mousedown,click等原生事件。 - 识别: 当事件触发,React 识别出是
click。 - 翻译: 调用
getEventPriority('click'),返回DiscreteEventPriority(Lane 1)。 - 调度: 调用
Scheduler_runWithPriority(Lane 1, callback)。这告诉调度器:“这是紧急任务,必须插队。” - 包装: 创建一个
SyntheticEvent对象(使用 Event Pool 优化性能)。 - 冒泡: 沿着 DOM 树向上冒泡,找到对应的 React Fiber 节点。
- 执行: 调用用户注册的
onClick回调函数。 - 更新: 回调函数修改状态,更新 Fiber 树。
- 渲染: 调度器根据优先级,决定何时将这些更新渲染到屏幕上。
这就是 React 事件优先级分发的全部秘密。它不仅仅是把 onclick 换了个马甲,它是构建 React 并发渲染引擎的基石。没有 Lane,就没有高优任务插队,就没有流畅的动画,就没有那种“点击反馈瞬间响应”的丝滑感。
下次当你点击屏幕上的按钮时,请记住,你不仅仅是点击了一个 <button>,你是在指挥一个庞大的、精密的、拥有 2^29 条车道的调度系统,为你服务的。
今天的讲座就到这里,下课!