Skip to content

虚拟事件系统实现

开始

React源码开篇章节中提到,当使用createRoot的时候,内部会调用listenToAllSupportedEvents方法。该方法在React-Dom/src/events/DOMPluginEventSystem.js文件中定义。所有React虚拟事件定义的内容都是在events这个文件夹下的。首先看一下DOMPluginEventSystem.js文件,当首次引入该文件的时候,会执行以下方法:

javascript
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

这些方法均在events/plugins文件夹下定义。

EventRegistry.js

在讨论上述5个registerEvents之前,先看一下EventRegistry.js文件。里面主要定义了两个函数:

registerDirectEvent

javascript
export function registerDirectEvent(
  // react 里面的事件名称 
  registrationName: string,
  // 真实节点的原生事件名称[数组]
  dependencies: Array<DOMEventName>,
) {
  registrationNameDependencies[registrationName] = dependencies;

  for (let i = 0;i < dependencies.length;i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

该函数主要做了两件事:

  • 建立registrationNamedependencies的映射关系,并存放到registrationNameDependencies中。
  • 将所有映射过的dependencies挨个添加到allNativeEvents

registerTwoPhaseEvent

javascript
export function registerTwoPhaseEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
): void {
  registerDirectEvent(registrationName, dependencies);
  registerDirectEvent(registrationName + 'Capture', dependencies);
}

该函数调用两次registerDirectEvent函数,第二次调用为注册的事件增加了Capture后缀。

registerEvents

再来看下5个registerEvenets的实现。

SimpleEventPlugin

SimpleEventPlugin.registerEvents调用的是registerSimpleEvents方法,在DomEventsProperties.js文件中定义:

javascript
export function registerSimpleEvents() {
  for (let i = 0; i < simpleEventPluginEvents.length; i++) {
    const eventName = ((simpleEventPluginEvents[i]: any): string);
    const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
    const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
    registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
  }
  // Special cases where event names don't match.
  registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
  registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
  registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
  registerSimpleEvent('dblclick', 'onDoubleClick');
  registerSimpleEvent('focusin', 'onFocus');
  registerSimpleEvent('focusout', 'onBlur');
  registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}

simpleEventPluginEvents是原生事件组成的一个数组:

javascript
const simpleEventPluginEvents = [
  'close',
  'copy',
  'cut',
  'drag',
  'mouseDown',
  // ... 等等一系列的原生事件
];

capitalizedEvent就是将原生事件名称的第一个字母大写,然后调用registerSimpleEvent方法:

javascript
function registerSimpleEvent(domEventName, reactName) {
  topLevelEventsToReactNames.set(domEventName, reactName);
  registerTwoPhaseEvent(reactName, [domEventName]);
}

首先会将原生事件名称domEventNameReact合成事件名称reactName存放到topLevelEventsToReactNames中。随后通过registerTwoPhaseEvent建立两者之间的关系。拿click来举例,注册后有:

javascript
// 原生事件 click
domEventName = 'click'
// 得到 react 的事件名称
reactName = 'onClick'
// 经过registerTwoPhaseEvent绑定关系后
registrationNameDependencies = {
  'onClick': ['click'],
  'onClickCapture': ['click']
}
allNativeEvents = new Set('click')

EnterLeaveEventPlugin

注册mouseout/mouseover等事件:

javascript
function registerEvents() {
  registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
  registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
  registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
  registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}

ChangeEventPlugin

注册onChange事件,这个事件对应了多个原生事件:

javascript
function registerEvents() {
  registerTwoPhaseEvent('onChange', [
    'change',
    'click',
    'focusin',
    'focusout',
    'input',
    'keydown',
    'keyup',
    'selectionchange',
  ]);
}

SelectEventPlugin

注册onSelect事件,这个事件对应了多个原生事件:

javascript
function registerEvents() {
  registerTwoPhaseEvent('onSelect', [
    'focusout',
    'contextmenu',
    'dragend',
    'focusin',
    'keydown',
    'keyup',
    'mousedown',
    'mouseup',
    'selectionchange',
  ]);
}

BeforeInputEventPlugin

注册了多个合成事件:

javascript
function registerEvents() {
  registerTwoPhaseEvent('onBeforeInput', [
    'compositionend',
    'keypress',
    'textInput',
    'paste',
  ]);
  registerTwoPhaseEvent('onCompositionEnd', [
    'compositionend',
    'focusout',
    'keydown',
    'keypress',
    'keyup',
    'mousedown',
  ]);
  registerTwoPhaseEvent('onCompositionStart', [
    'compositionstart',
    'focusout',
    'keydown',
    'keypress',
    'keyup',
    'mousedown',
  ]);
  registerTwoPhaseEvent('onCompositionUpdate', [
    'compositionupdate',
    'focusout',
    'keydown',
    'keypress',
    'keyup',
    'mousedown',
  ]);
}

最后

执行完以上5个注册函数后,allNativeEvents会记录所有已注册的原生事件。registrationNameDependencies则记录所有原生事件和React合成事件之间的关系。

listenToAllSupportedEvents

listenToAllSupportedEvents开始真正监听原生事件。首先会判断有没有注册过,保证原生事件只会注册一次:

javascript
if (!(rootContainerElement: any)[listeningMarker]) {
  (rootContainerElement: any)[listeningMarker] = true;
}

然后开始遍历原生事件,进行监听:

javascript
allNativeEvents.forEach(domEventName => {
  if (!nonDelegatedEvents.has(domEventName)) {
    listenToNativeEvent(domEventName, false, rootContainerElement);
  }
  listenToNativeEvent(domEventName, true, rootContainerElement);
}

selectionchange事件不会进行事件冒泡,需要单独在document上注册。而其他事件则通过listenToNativeEvent函数注册。

javascript
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget,
): void {
  let eventSystemFlags = 0;
  // 是否是捕捉事件阶段
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener,
  );
}

addTrappedEventListener函数首先会根据事件的名称获取相应的优先级,然后将listener包装成按优先级执行的listener

javascript
// 通过事件名称,设置不同的优先级  
let listener = createEventListenerWrapperWithPriority(
  targetContainer,
  domEventName,
  eventSystemFlags,
);

createEventListenerWrapperWithPriority函数的代码如下:

javascript
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
 domEventName: DOMEventName,
 eventSystemFlags: EventSystemFlags,
): Function {
  // 获取事件优先级
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  // 返回包装了优先级的listener
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

getEventPriority定义如下:

javascript
export function getEventPriority(domEventName: DOMEventName): * {
  switch (domEventName) {
    case 'click':
    case 'keydown':
    case 'mouseup':
    case 'paste':
    // ... 等等一系列事件
    // 均为同步优先级
    return DiscreteEventPriority;
    case 'drag':
    case 'mousemove':
    case 'pointermove':
    // ... 等等一系列事件
    // 均为连续事件优先级
    return ContinuousEventPriority;
    case 'message': 
    //  ... 获取 scheduler callback 的优先级
    default:
    return DefaultEventPriority;
  }
}

该函数主要将原生事件分为了DiscreteEventPriorityContinuousEventPriority两大类。

包装后的dispatchDiscreteEventdispatchContinuousEvent则是在执行代码前会设置全局的优先级,这样在更新的时候通过requestUpdateLane可以获取到更新对应的优先级。例如:

javascript
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);

这里的dispatchEvent就是事件回调执行的内容,后面会提到。

包装完listener后就需要给DOM节点添加原生事件了,这个DOM节点就是container,即React渲染内容的容器。

javascript
if (isCapturePhaseListener) {
 // 捕捉事件阶段
  unsubscribeListener = addEventCaptureListener(
    targetContainer,
    domEventName,
    listener,
  );
} else {
  // 冒泡阶段阶段
  unsubscribeListener = addEventBubbleListener(
    targetContainer,
    domEventName,
    listener,
  );
}

分别调用DOM的原生方法监听原生捕获事件和冒泡事件:

javascript
// 冒泡事件 
target.addEventListener(eventType, listener, false);
// 捕获事件
target.addEventListener(eventType, listener, true);

dispatchEvent

当真实点击事件触发时,会调用监听函数dispatchEvent,该函数调用attemptToDispatchEvent函数:

javascript
export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
  // TODO: Warn if _enabled is false.

  // 1. 根据事件获取到触发该事件的 目标节点
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 2. 通过真实节点找到最近的 fiber
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null) {
    // 3. 如果 fiber 存在,看是否已经挂载且在 HostRoot 内部,确保fiber合适。
    const nearestMounted = getNearestMountedFiber(targetInst);
    if (nearestMounted === null) {
      targetInst = null;
    }
    // ...其他处理
  }
  // 4. 触发回调
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  return null;
}

首先会根据原生事件触发时的event获取到相应的触发时的目标元素。随后根据真实节点找到对应的fiber

javascript
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
  let targetInst = (targetNode: any)[internalInstanceKey];
  if (targetInst) {
    return targetInst;
  }
 // ...
}

completeWork阶段创建真实节点时:

javascript
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  // 创建了 element
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  // node . '__reactFiber$' + randomKey = fiber
  precacheFiberNode(internalInstanceHandle, domElement);
  // node . '__reactProps$' + randomKey = props
  updateFiberProps(domElement, props);
  return domElement;
}

会执行precacheFiberNode方法和updateFiberProps方法,在真实DOM节点上存放fiberprops,因此事件触发时可以获取到对应的fiber。拿到合适的fiber后就可以开始触发fiberprops里的事件了。

dispatchEventForPluginEventSystem

对于能够进行事件冒泡的事件,通常会执行下列分支代码:

javascript
if (
  (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
  (eventSystemFlags & IS_NON_DELEGATED) === 0
) {}

该分支会向上遍历fiber,如果遇到了HostRoot或者HostPortal,并且container是一致的:

javascript
isMatchingRootContainer(container, targetContainerNode)

那么说明该事件是在container内部触发的,可以直接触发回调。这里为什么containerHostPortal也能监听事件呢?因为在completeWork阶段有这样一行代码-preparePortalMount:

javascript
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
  preparePortalMount(workInProgress.stateNode.containerInfo);
}
bubbleProperties(workInProgress);
return null;

currentnull的时候,会在portalcontainer上监听原生事件:

javascript
export function preparePortalMount(portalInstance: Instance): void {
  listenToAllSupportedEvents(portalInstance);
}

除此之外,如果找不到正确的container,那么将不会执行事件回调。

监测完container后,将执行dispatchEventsForPlugins函数:

javascript
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
  dispatchQueue,
  domEventName,
  targetInst,
  nativeEvent,
  nativeEventTarget,
  eventSystemFlags,
  targetContainer,
);

extractEvents

extractEvents方法会调用SimpleEvents.extractEvents。首先获取原生事件对应的reactName

javascript
// 1. 根据事件名称,获取 react 中对应的事件名称
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
  return;
}

然后根据原生事件名称获取react中要返回的事件的参数的结构:

javascript
// 2. 根据事件名称,获取需要返回的内容的结构
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
switch (domEventName) {
  case 'keydown':
  case 'keyup':
    SyntheticEventCtor = SyntheticKeyboardEvent;
    break;
  case 'focusin':
    reactEventType = 'focus';
    SyntheticEventCtor = SyntheticFocusEvent;
    break;
  case 'touchcancel':
  case 'touchend':
  case 'touchmove':
  case 'touchstart':
    SyntheticEventCtor = SyntheticTouchEvent;
    break;
    // ...等等一些列合成事件结构
    // ...等等一些列合成事件结构
    // ...等等一些列合成事件结构
}

createSyntheticEvent

合成事件返回的结构(即自定义的一个event对象)通过createSyntheticEvent定义,它接收一个定义好的参数结构,简化后的代码如下:

javascript
function createSyntheticEvent(Interface: EventInterfaceType) {
  function SyntheticBaseEvent(
   reactName: string | null,
   reactEventType: string,
   targetInst: Fiber,
   nativeEvent: {[propName: string]: mixed},
   nativeEventTarget: null | EventTarget,
  ) {
    // ...
    return this;
  }

  Object.assign(SyntheticBaseEvent.prototype, {
    // 阻止默认行为
    preventDefault: function() {
      this.defaultPrevented = true;
      const event = this.nativeEvent;
      if (event.preventDefault) {
        event.preventDefault();
      } else if (typeof event.returnValue !== 'unknown') {
        event.returnValue = false;
      }
      this.isDefaultPrevented = functionThatReturnsTrue;
    },
    // 阻止事件冒泡
    stopPropagation: function() {
      const event = this.nativeEvent;
      if (event.stopPropagation) {
        event.stopPropagation();
      } else if (typeof event.cancelBubble !== 'unknown') {
        event.cancelBubble = true;
      }

      this.isPropagationStopped = functionThatReturnsTrue;
    },

  });
  return SyntheticBaseEvent;
}

该方法定义了一个SyntheticBaseEvent对象,并在该对象上添加了preventDefault方法和stopPropagation方法。

javascript
function SyntheticBaseEvent(
  // react 中监听事件的名称
  reactName,
  // 原生事件名称
  reactEventType,
  // 触发事件的节点对应的 fiber
  targetInst,
  // 原生事件触发时返回的 event 参数
  nativeEvent,
  // 原生事件节点
  nativeEventTarget,
) {
  this._reactName = reactName;
  this._targetInst = targetInst;
  this.type = reactEventType;
  this.nativeEvent = nativeEvent;
  this.target = nativeEventTarget;
  this.currentTarget = null;

  for (const propName in Interface) {
    if (!Interface.hasOwnProperty(propName)) {
      continue;
    }
    // 如果normalize存在,说明他有自定义的格式化方法
    const normalize = Interface[propName];
    if (normalize) {
      this[propName] = normalize(nativeEvent);
    } else {
      // 否则,直接使用原生的值
      this[propName] = nativeEvent[propName];
    }
  }

  const defaultPrevented =
        nativeEvent.defaultPrevented != null
  ? nativeEvent.defaultPrevented
  : nativeEvent.returnValue === false;
  if (defaultPrevented) {
    this.isDefaultPrevented = functionThatReturnsTrue;
  } else {
    this.isDefaultPrevented = functionThatReturnsFalse;
  }
  this.isPropagationStopped = functionThatReturnsFalse;
  return this;
}

该对象会通过不同的Interfaceevent对象设置不同的属性,并且还可以自定义一些”格式化“的函数,方便取值。

紧接着调用accumulateSinglePhaseListeners计算所有需要触发事件的listener,大致思路是从下向上遍历找到合适的节点,通过节点创建一个listener,最后推入到数组当中:

javascript
if (tag === HostComponent && stateNode !== null) {
  // 1. 当前遍历的真实节点
  lastHostComponent = stateNode;
  if (reactEventName !== null) {
    // 2. 拿到在 react 中定义的事件回调
    const listener = getListener(instance, reactEventName);
    if (listener != null) {
      // 3. 创建一个 listener,并添加到 listeners 中
      listeners.push(
        createDispatchListener(instance, listener, lastHostComponent),
      );
    }
  }
}

其中getListener定义如下:

javascript
const props = getFiberCurrentPropsFromNode(stateNode);
const listener = props[registrationName];

首先从node节点上获取对应的props,然后根据registrationName获取在react中注册的事件回调。最后创建一个listener

javascript
{
  instance, // 当前的 fiber
  listener, // 当前的事件回调
  currentTarget, // 当前的真实节点
}

最后会形成一个从当前节点到根节点之间的一个listeners数组

如果这个数组长度大于0,那么会根据当前事件类型创建一个event对象,将事件回调和事件对象加入到dispatchQueue中:

javascript
if (listeners.length > 0) {
  const event = new SyntheticEventCtor(
    reactName,
    reactEventType,
    null,
    nativeEvent,
    nativeEventTarget,
  );
  dispatchQueue.push({ event, listeners });
}

processDispatchQueue

最后执行processDispatchQueue方法,触发事件回调:

javascript
export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0;i < dispatchQueue.length;i++) {
    const { event, listeners } = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
}

processDispatchQueueItemsInOrder在触发的时候会根据inCapturePhase判断当前是事件捕捉还是事件冒泡阶段,从而决定listeners的触发顺序:

javascript
for (let i = 0;i < dispatchListeners.length;i++) {
  const { instance, currentTarget, listener } = dispatchListeners[i];
  // 是否阻止事件冒泡
  if (instance !== previousInstance && event.isPropagationStopped()) {
    return;
  }
  executeDispatch(event, listener, currentTarget);
  previousInstance = instance;
}

executeDispatch执行react事件回调,并且执行的时候会将新构建的event对象作为参数传递。

总结

整个虚拟事件系统的过程大致为:

  1. 定义react合成事件名称,建立起合成事件名称和原生事件名称的关系。
  2. 监听事件:
  • 如果是HostRoot,创建时就监听所有原生事件。(包括捕获事件和冒泡事件)
  • 如果是HostPortal,在completeWork阶段判断current === nulltrue时监听所有事件。
  • 如果是不可代理的事件,需要在completeWork创建真实节点时,”手动“添加原生事件。
  1. 触发事件:
  • 触发原生事件时,root接收到该事件,此时能拿到具体触发事件的真实DOM元素。
  • 通过真实DOM获取到相应的fiber,并进行一系列的校验过程。
  • 根据原生事件名称获取对应的react事件名称。从当前fiber向上查找到根节点,将所有监听了该事件的回调保存起来形成listeners
  • 根据原生的event对象创建一个合成的event对象,可以自定义部分属性。
  • 最后根据是捕捉阶段还是冒泡阶段,决定要执行监听事件和listeners的执行顺序。

案例

演示代码

javascript
export default class extends React.Component {
  componentDidMount() {
    document.addEventListener('mousedown', () => {
      console.log('mouse down event ====>', 'document');
      Promise.resolve().then(() => console.log('promise event ====>', 'document'));
    });

    document.addEventListener('click', () => {
      console.log('click event ====>', 'document');
    });
  }

  handleClick(e) {
    const id = e.currentTarget.dataset.id;
    console.log('click event ====>', id);
  }

  handleMousedown(e) {
    const id = e.currentTarget.dataset.id;
    console.log('mouse down event ====>', id);
    Promise.resolve().then(() => console.log('promise event ====>', id));
  }

  render() {
    return (
      <div onClick={this.handleClick} onMouseDown={this.handleMousedown} data-id="parent">
        <div onClick={this.handleClick} onMouseDown={this.handleMousedown} data-id="child">
          点击事件和 promise 回调
        </div>
      </div>
    );
  }
}

运行结果

javascript
// React 16.x 运行结果
mouse down event ====> child
mouse down event ====> parent
promise event ====> child
promise event ====> parent
mouse down event ====> document
promise event ====> document
click event ====> child
click event ====> parent
click event ====> document

结果分析

此处以 React16 为例,React16 是在 document 上监听处理合成事件,而 React17+ 是在 root container 上监听处理合成事件,略有不同。

  • child 被点击时,原生事件 mousedown 冒泡到 document 上。
  • 首先触发 document 上处理合成事件的回调,此时为一个宏任务。
    • 合成事件执行:child => parent,其中产生的微任务,在本次宏任务结束时执行。
  • 其次触发 document 上绑定的其他事件,此时为一个宏任务。
    • 触发 mouse down event ====> document,结束后执行微任务。
  • mousedown 执行完成后,开始触发 click 事件回调。基本思路同 mousedown

一些结论

  • 合成事件的回调为宏任务,冒泡的过程也是在一个宏任务中进行的。
  • 原生事件的回调为宏任务,冒泡的时候,每个节点上监听的原生事件回调都是一个宏任务。
    • 如:child1 => promise1 => child2 => promise2 => parent1 => promise3 => ...
  • 原生事件如果用 button.click() 这种 js 语法调用时,其执行结果区别人 addEventListener 监听的结果。
    • 如:child1 => child2 => promise1 => promise2 => parent1 => ...
    • 原因是 js 语法调用时,相当于同步触发 listeners

参考