事件

53 阅读9分钟

首先定义入口文件,使用函数组件。

function FunctionComponent() {
  return (
    <h1 onClick={() => console.log("父冒泡")} onClickCapture={() => console.log("父捕获")}>
      <span onClick={() => console.log("子冒泡")} onClickCapture={() => console.log("子捕获")}>
        test
      </span>
    </h1>
  );
}

const element = <FunctionComponent />;

const root = createRoot(document.getElementById("root"));
root.render(element);

简单处理函数组件

要处理函数组件,需要根据 Fiber 节点 tag 进行处理。在 beginWork 函数中做判断。

export function beginWork(current, workInProgress) {
  switch (workInProgress.tag) {
    case IndeterminateComponent:
      return mountIndeterminateComponent(current, workInProgress, workInProgress.type);
    //...
  }
}

主要逻辑在 mountIndeterminateComponent 中。

function mountIndeterminateComponent(current, workInProgress, Component) {
  console.log("挂载函数组件");
  const props = workInProgress.pendingProps;
  // const value = Component(props);
  const value = renderWithHooks(current, workInProgress, Component, props);
  workInProgress.tag = FunctionComponent;
  reconcileChildren(current, workInProgress, value);
  return workInProgress.child;
}

对于当前阶段,先简单处理一下。单独定义 renderWithHooks 函数方便以后扩展。

export function renderWithHooks(current, workInProgress, Component, props) {
  const children = Component(props);
  return children;
}

这样就可以了,对于函数组件的简单渲染。

合成事件

在一开始的入口文件中,定义了一个函数式组件,其中返回了两个元素,这两个元素分别绑定了捕获和冒泡事件。分别为:

  • 父捕获
  • 子捕获
  • 子冒泡
  • 父冒泡 对于 React 的期望来说,需要做的就是将定义的回调函数按照原生事件执行顺序执行,也就是上面列的顺序。

截屏2025-06-30 19.51.31.png

需要实现这个模型,在 react 中通过事件代理实现。

实现合成事件的核心文件是 react-dom-bindings/src/events/DOMPluginEventSystem.js。该文件导出 listenToAllSupportedEvents 函数,并且在 createRoot 函数中进行调用。

准备阶段

在准备阶段,需要记录需要处理哪些事件,比如 click,drop 等等。

入口在 react-dom-bindings/src/events/DOMPluginEventSystem.js

在这个文件中会调用 SimpleEventPlugin.registerEvents 函数,SimpleEventPlugin 是一个插件,位于 react-dom-bindings/src/events/plugins/SimpleEventPlugin.js

对于 registerEvents 函数,这个插件其并没有做处理,只是做了一层转包。

// 删除其他不相干代码
import { registerSimpleEvents } from "../DOMEventProperties";
export { registerSimpleEvents as registerEvents };

可以看到,registerEvents 函数其实是来自 DOMEventProperties 文件导出的 registerSimpleEvents 函数。

import { registerTwoPhaseEvent } from "./EventRegistry";

const simpleEventPluginEvents = ["click"];
export const topLevelEventToReactNames = new Map();
function registerSimpleEvent(domEventName, reactName) {
  topLevelEventToReactNames.set(domEventName, reactName);
  registerTwoPhaseEvent(reactName, [domEventName]);
}

export function registerSimpleEvents() {
  for (let i = 0; i < simpleEventPluginEvents.length; i++) {
    const eventName = simpleEventPluginEvents[i];
    const domEventName = eventName.toLowerCase();
    const capitalizeEventName = eventName[0].toUpperCase() + eventName.slice(1);
    registerSimpleEvent(domEventName, `on${capitalizeEventName}`);
  }
}

这个文件中其实就干了两件事。

  • 处理 topLevelEventToReactNames
  • 调用 registerTwoPhaseEvent 函数

topLevelEventToReactNames 变量事一个 map 键值对。key 为原生事件名,value 为 react 事件名。

例如 click - onClick。

// 由于 registerTwoPhaseEvent 逻辑相对简单,就放在这里自行阅读。
export const allNativeEvents = new Set();

/**
 * 注册两个阶段的事件
 * @param {*} registrationName React 事件名 onClick
 * @param {*} dependencies 原生事件数组 ['click']
 */
export function registerTwoPhaseEvent(registrationName, dependencies) {
  // 注册冒泡事件的对应关系
  registerDirectEvent(registrationName, dependencies);
  // 注册捕获事件的对应关系
  registerDirectEvent(registrationName + "Capture", dependencies);
}

export function registerDirectEvent(registrationName, dependencies) {
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

以上,就是准备阶段的工作。

挂载阶段

一开始说过,合成事件是通过事件代理模拟原生事件执行逻辑。事件代理需要将事件处理器绑定到目标事件触发对象的父级元素上,在 react 项目中,有一个根元素满足这个条件,那就是 div#root。所以需要将事件处理器绑定到这个元素上。

对应到代码中则是在 creteRoot 函数中调用的 listenToAllSupportedEvents 函数。

const listeningMarker = "__reactListening" + Math.random().toString(36).slice(2);
export function listenToAllSupportedEvents(rootContainerElement) {
  // 监听所有事件,只监听一次
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    allNativeEvents.forEach((domEventName) => {
      listenToNativeEvent(domEventName, true, rootContainerElement);
      listenToNativeEvent(domEventName, false, rootContainerElement);
    });
  }
}

这个函数接收一个 rootContainerElement 参数,那这个参数是什么呢?

这个参数其实就是 div#root 元素。上面说过,这个函数是在 creteRoot 函数中调用的。在下面的代码中可以看到,调用 listenToAllSupportedEvents 函数时传入了 container 参数。

同时 creteRoot 函数是在 main.ts 入口文件调用的,传入的参数就是 div#root 元素。

export function createRoot(container) {
  const root = createContainer(container);
  listenToAllSupportedEvents(container);
  return new ReactDOMRoot(root);
}

listenToAllSupportedEvents 函数中使用了一个变量 listeningMarker 当作 rootContainerElement 的 key。并赋值为 true,以作一个判断,当已经赋值过,表示已经挂载过了,就不用再次挂载。变量 listeningMarker是一个随机值。

同时使用了变量 allNativeEvents。这是在 registerTwoPhaseEvent 函数中进行处理,位于 react-dom-bindings/src/events/EventRegistry.js。这是一个 Set 变量,存储所有需要监听的原生事件。例如 click 事件。

接下来就是遍历这个 Set 对其中的每个事件进行监听。即调用 listenToNativeEvent 函数。

/**
 * 注册原生事件
 * @param {*} domEventName 事件名
 * @param {*} isCapturePhaseListener 是否是捕获事件
 * @param {*} target 目标DOM 节点
 */
export function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
  let eventSystemFlags = 0;
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }

  addTrappedEventListener(domEventName, isCapturePhaseListener, target, eventSystemFlags);
}

在这个函数中,使用了两个变量,eventSystemFlagsIS_CAPTURE_PHASE。这两个变量是二进制数字,表示是否为捕获阶段。然后调用 addTrappedEventListener 函数。

function addTrappedEventListener(domEventName, isCapturePhaseListener, targetContainer, eventSystemFlags) {
  const listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
  if (isCapturePhaseListener) {
    addEventCaptureListener(targetContainer, domEventName, listener);
  } else {
    addEventBubleListener(targetContainer, domEventName, listener);
  }
}

这个函数逻辑也相对简单,首先创建一个 listener,这个就是事件处理器,也就是回掉函数,接下来根据事件是否为捕获调用不同的挂载函数。这两个函数就是执行原生挂载,将生成的 listener 挂载到对应的原生事件回掉函数上。

export function addEventCaptureListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, true);
  return listener;
}

export function addEventBubleListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, false);
  return listener;
}

至此,挂载阶段完成了,接下来需要看执行阶段。

执行阶段

执行阶段,其实就是用户点击元素后触发事件,执行回调函数的阶段。这个时候执行的回调函数其实就是上面的 listener。所以,接下来需要搞清楚这个 listener 是如何被创建的。

export function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
  const listenerWrapper = dispatchDiscreteEvents;
  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}

根据代码可知,createEventListenerWrapperWithPriority 返回了一个新的函数,这个新函数其实是绑定了 this 为 null 且绑定了 domEventName, eventSystemFlags, targetContainer 三个参数的 dispatchDiscreteEvents 函数。

于是可以下一个结论,在用户点击时,就是执行的 dispatchDiscreteEvents 函数。

/**
 * 派发离散事件的监听函数
 * @param {*} domEventName 事件名
 * @param {*} eventSystemFlags 系统标志 冒泡或捕获
 * @param {*} container 容器
 * @param {*} nativeEvent 原生事件
 */
function dispatchDiscreteEvents(domEventName, eventSystemFlags, container, nativeEvent) {
  dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}

function dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent) {
  // 获取事件源
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 获取对应 Fiber 实例
  const targetInst = getClosestInstanceFromNode(nativeEventTarget);

  dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, container);
}

由以上代码可以看到,dispatchDiscreteEvents 其实接收 4 个参数,其中前三个在 createEventListenerWrapperWithPriority 函数中已经被绑定,第四个 nativeEvent 其实是由 addEventListener 传入。即原生事件对象。

dispatchDiscreteEvents 会将这些参数透传给 dispatchEvent 函数。

dispatchEvent 首先会获取事件源。

export default function getEventTarget(nativeEvent) {
  return nativeEvent.target || nativeEvent.srcElement || window;
}

然后会获取对应的 fiber 节点。这里可以根据之前缓存的数据直接获取。

/**
 * 从真实DOM 节点上获取对应的 fiber
 * @param {*} targetNode
 * @returns
 */
export function getClosestInstanceFromNode(targetNode) {
  const targetInst = targetNode[internalInstanceKey];
  return targetInst;
}

/**
 * 提前缓存 Fiber 节点实例到 DOM 节点,在创建真实 DOM 时执行
 * @param {*} hostInst
 * @param {*} node
 */
export function precacheFiberNode(hostInst, node) {
  node[internalInstanceKey] = hostInst;
}

最后执行 dispatchEventForPluginEventSystem 函数。


export function dispatchEventForPluginEventSystem(
  domEventName,
  eventSystemFlags,
  nativeEvent,
  targetInst,
  targetContainer
) {
  dispatchEventForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
}

function dispatchEventForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 派发事件的数组
  const dispatchQueue = [];
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

dispatchEventForPluginEventSystem 函数只是调用了 dispatchEventForPlugins 函数并透传参数。

dispatchEventForPlugins 函数第一件事就是维护一个事件队列。

对于要实现原生事件执行顺序的需求来说,根据一开始的执行顺序图,可以想象,如果拿到从出发点事件元素到最外层元素的所有事件处理函数,并放到一个队列中,那么对于捕获事件来说,就是反着执行一遍,对于冒泡事件来说,就是正着执行一遍。

extractEvents 函数就是对 dispatchQueue 进行处理。

function extractEvents(
  dispatchQueue,
  domEventName,
  targetInst,
  nativeEvent,
  nativeEventTarget,
  eventSystemFlags,
  targetContainer
) {
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );
}
/**
 * 把要执行的回掉函数添加到 dispatchQueue 中
 * @param {*} dispatchQueue 派发队列,里面放置回掉函数
 * @param {*} domEventName DOM 事件名称
 * @param {*} targetInst 目标 fiber
 * @param {*} nativeEvent 原生事件
 * @param {*} nativeEventTarget 原生事件源
 * @param {*} eventSystemFlags 系统标识
 * @param {*} targetContainer 目标容器
 */
function extractEvents(
  dispatchQueue,
  domEventName,
  targetInst,
  nativeEvent,
  nativeEventTarget,
  eventSystemFlags,
  targetContainer
) {
  const reactName = topLevelEventToReactNames.get(domEventName);
  // 合成事件构造函数
  let SyntheticEventCtor;
  switch (domEventName) {
    case "click":
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    default:
      break;
  }

  const isCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  const listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, isCapturePhase);

  if (listeners.length > 0) {
    const event = new SyntheticEventCtor(reactName, domEventName, targetInst, nativeEvent, nativeEvent.target);
    dispatchQueue.push({ event, listeners });
  }
}

extractEvents 函数调用了 SimpleEventPlugin 中导出的 extractEvents 函数。

extractEvents 函数一开始根据原生事件名获取对应的 react 事件名。

再根据 react 事件名选取对应的合成事件构造函数,并赋值给 SyntheticEventCtor 变量。

再根据 eventSystemFlags 标识判断是否为捕获。

然后会调用 accumulateSinglePhaseListeners 函数获取所有 react 事件绑定的函数。

export function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, isCapturePhase) {
  const captureName = reactName + "Capture";
  const reactEventName = isCapturePhase ? captureName : reactName;
  const listeners = [];
  let instance = targetFiber;
  while (instance !== null) {
    const { stateNode, tag } = instance;
    if (tag === HostComponent && stateNode !== null) {
      const listener = getListener(instance, reactEventName);
      if (listener != null) {
        listeners.push(createDispatchListener(instance, listener, stateNode));
      }
    }
    instance = instance.return;
  }
  return listeners;
}

accumulateSinglePhaseListeners 函数的逻辑就是从当前节点开始,逐层向上查找,获取每个元素的 listener

接下来就是在存在 listeners 的情况下,创建 SyntheticEventCtor 实例,这是合成事件类。然后推入 dispatchQueue

import assign from "shared/assign";
function functionThatReturnsTrue() {
  return true;
}

function functionThatReturnsFalse() {
  return false;
}

const MouseEventInterface = {
  screenX: 0,
  screenY: 0,
  clientX: 0,
  clientY: 0,
  pageX: 0,
  pageY: 0,
  ctrlKey: false,
  shiftKey: false,
  altKey: false,
  metaKey: false,
  getModifierState: function (key) {},
  button: 0,
  buttons: 0,
};

function createSyntheticEvent(inter) {
  /**
   * 合成事件的基类
   * @param {*} reactName React 属性名 onClick
   * @param {*} reactEventType click
   * @param {*} targetInst 事件源对应的 fiber 实例
   * @param {*} nativeEvent 原生事件对象
   * @param {*} nativeEventTarget 原生事件源对应的真实DOM
   */
  function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
    this._reactName = reactName;
    this.type = reactEventType;
    this._targetInst = targetInst;
    this._nativeEvent = nativeEvent;
    this.target = nativeEventTarget;

    for (const propName in inter) {
      if (!inter.hasOwnProperty(propName)) {
        continue;
      }
      this[propName] = nativeEvent[propName];
    }
    // 是否已经阻止默认行为
    this.isDefaultPrevented = functionThatReturnsFalse;
    // 是否阻止继续传播
    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  assign(SyntheticBaseEvent.prototype, {
    preventDefault() {
      const event = this.nativeEvent;
      if (event.preventDefault) {
        event.preventDefault();
      } else {
        event.returnValue = false;
      }
      this.isDefaultPrevented = functionThatReturnsTrue;
    },
    stopPropagation() {
      const event = this.nativeEvent;
      if (event.stopPropagation) {
        event.stopPropagation();
      } else {
        event.cancelBubble = true;
      }
      this.isPropagationStopped = functionThatReturnsTrue;
    },
  });

  return SyntheticBaseEvent;
}

export const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface);

这里有一个合成事件基类。通过给 createSyntheticEvent 函数传入不同的参数生成不同的合成事件类。这个函数主要是绑定属性到 react 事件和原型链上,包括原生属性,和一些内置属性,还有两个方法。

接下来就可以去执行了。

function processDispatchQueue(dispatchQueue, eventSystemFlags) {
  // 判断是否在捕获阶段
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    const { event, listeners } = dispatchQueue[i];
    processDispatchQueueItemInOrder(event, listeners, inCapturePhase);
  }
}

function executeDispatch(event, listener, currentTarget) {
  event.currentTarget = currentTarget;
  listener(event);
}

function processDispatchQueueItemInOrder(event, listeners, inCapturePhase) {
  if (inCapturePhase) {
    for (let i = listeners.length - 1; i >= 0; i--) {
      const { listener, currentTarget } = listeners[i];
      if (event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
    }
  } else {
    for (let i = 0; i < listeners.length; i++) {
      const { listener, currentTarget } = listeners[i];
      if (event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
    }
  }
}

核心逻辑是遍历 dispatchQueue,拿到 react 事件实例和对应的 listeners。然后根据是否是捕获来正向或者反向执行。捕获反向执行,冒泡正向执行。listeners 中每个 listener 都是 react 事件的回调函数,在执行时将 react 事件实例传入即可。

结束

以上只针对 click 事件进行举例说明。