React 事件优先级分发:当用户点击按钮时,React 源码是如何将原生事件包装并映射到特定 Lane 优先级的?

各位同学好,欢迎来到今天的“React 源码深度解析”现场。我是你们的讲师。

今天我们不讲 useEffect 的依赖陷阱,也不讲 Context 的性能大坑。我们要聊的是 React 的“神经系统”——事件系统

尤其是当你在屏幕上疯狂点击按钮,React 是如何在毫秒级的时间内,把一个原生的 DOM 事件“包装”成一个 React 事件,并把它塞进那个复杂的 Lane(车道)调度系统里的?这简直就像是一场精心编排的特工行动。

准备好了吗?系好安全带,我们要钻进 React 的核心代码库了。


第一部分:伪装的艺术——原生事件是如何变成 SyntheticEvent 的?

在 React 出现之前,我们直接监听 DOM 事件。但在 React 出现之后,事情变得有点复杂。React 告诉你:“别去管 onclick,那是个假象。”

React 的策略是:监听原生事件,包装成合成事件,冒泡机制照旧。

1. 拦截与伪装

React 不会给每一个 button 挂载一个 addEventListener。那样太重了,性能太差。相反,React 在根节点(通常是 document 或挂载点)上只挂载了几个监听器。这些监听器监听的是什么呢?是那些浏览器原生的、高频触发的事件,比如 mousedownmouseupclickfocusinput 等等。

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,并提供了 preventDefaultstopPropagation 等方法。

代码大概长这样(简化):

// 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(最高),UserBlockingPriorityNormalPriority。这就像高速公路只有三条车道:快车道、慢车道、普通车道。

但在并发模式下,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();

运行结果预览:
当你点击按钮时,你会看到:

  1. 🔍 [分发器] 检测到原生事件: click, 优先级: Lane 1
  2. 🚀 [调度器] 接收到优先级为 Lane 1 的任务,开始执行...
  3. ⚡ [执行] 在 Fiber 节点 btn-1 上触发回调
  4. 🔥 用户点击了按钮!

当你移动鼠标时,你会看到:

  1. 🔍 [分发器] 检测到原生事件: mousemove, 优先级: Lane 2
  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 为了性能做出的巨大牺牲。


第九部分:总结与回顾

好了,各位同学,让我们把镜头拉远,看看整个流程的全景图。

  1. 拦截: React 在 document 上监听 mousedown, click 等原生事件。
  2. 识别: 当事件触发,React 识别出是 click
  3. 翻译: 调用 getEventPriority('click'),返回 DiscreteEventPriority(Lane 1)。
  4. 调度: 调用 Scheduler_runWithPriority(Lane 1, callback)。这告诉调度器:“这是紧急任务,必须插队。”
  5. 包装: 创建一个 SyntheticEvent 对象(使用 Event Pool 优化性能)。
  6. 冒泡: 沿着 DOM 树向上冒泡,找到对应的 React Fiber 节点。
  7. 执行: 调用用户注册的 onClick 回调函数。
  8. 更新: 回调函数修改状态,更新 Fiber 树。
  9. 渲染: 调度器根据优先级,决定何时将这些更新渲染到屏幕上。

这就是 React 事件优先级分发的全部秘密。它不仅仅是把 onclick 换了个马甲,它是构建 React 并发渲染引擎的基石。没有 Lane,就没有高优任务插队,就没有流畅的动画,就没有那种“点击反馈瞬间响应”的丝滑感。

下次当你点击屏幕上的按钮时,请记住,你不仅仅是点击了一个 <button>,你是在指挥一个庞大的、精密的、拥有 2^29 条车道的调度系统,为你服务的。

今天的讲座就到这里,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注