Skip to content

事件触发

React中对于合成事件的触发不是触发在目标对象上,而是通过事件委托的方式,将事件的触发统一放到FiberRootNode根节点上,然后根据 target['__reactFiber$' + randomKey] 获取目标 FiberNode 节点实例对象,再通过向上遍历的方式获取到从目标节点到Root上注册的此事件的listener对象,从而构建出整个事件流,最后通过processDispatchQueue对事件进行回调执行。

所有事件触发的核心方法都是 dipatchEvent 函数

dipatchEvent

从上面我们也就可以大体了解React事件的执行流程了,主要分为以下步骤

  • 根据target抹平平台差异获取到真正的 target对象
  • 根据target(DOM)获取到completeWork阶段缓存到DOM节点上的 目标元素的 FiberNode 对象
  • 通过FiberNode.return循环遍历出这个链上注册事件的 listener 对象
  • 按照事件的Capture 的不同或从前到后 或 从后到前遍历执行 listener

同时我们也要知道React合成事件的特点:

  • React事件的池化
  • React的事件event对象是共享的,在事件执行完成后就会被销毁

image

attemptToDispatchEvent()

其主要作用是:

  1. 获取事件的 target , 兼容性处理 IE 为 srcElement

  2. 获取目标元素对应的 FiberNode 对象,并做一定的处理

    1. 如果是有插入Placement副作用的元素节点, targetInst = null
    2. 如果是 HostRoot 根节点,return 单独处理
    3. 如果是 SuspenseComponent 类型的节点 获取其 suspenseState.dehydrated
js
// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
/**
 *  其任务是
 *  1.  获取事件的 target , 兼容性处理 IE 为 srcElement
 *  2.  获取目标元素对应的 FiberNode 对象,并做一定的处理
 *      - 如果是有插入Placement副作用的元素节点, targetInst = null
 *      - 如果是 HostRoot 根节点,return 单独处理
 *      - 如果是 SuspenseComponent 类型的节点  获取其 suspenseState.dehydrated
 *
 *  触发 dispatchEventForPluginEventSystem()
 * @param {*} domEventName
 * @param {*} eventSystemFlags
 * @param {*} targetContainer
 * @param {*} nativeEvent
 * @returns
 */
export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
  // TODO: Warn if _enabled is false.
  // 获取事件的 target , 兼容性处理 IE 为 srcElement
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 获取目标元素对应的FiberNode实例对象
  // 取巧的方式 在completeWork创建DOM的过程中会将其FiberNode对象缓存到DOM的"__reactFiber$h7mblvmf5m"属性上
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  // 对于目标FiberNode 进行判断
  //  1. 是否为有插入effect的元素节点 ,还没有插入到浏览器 怎么触发事件
  //  2. SuspenseComponent 类型的
  if (targetInst !== null) {
    const nearestMounted = getNearestMountedFiber(targetInst);
    if (nearestMounted === null) {
      // This tree has been unmounted already. Dispatch without a target.
      targetInst = null;
    } else {
      const tag = nearestMounted.tag;
      if (tag === SuspenseComponent) {
        const instance = getSuspenseInstanceFromFiber(nearestMounted);
        if (instance !== null) {
          // Queue the event to be replayed later. Abort dispatching since we
          // don't want this event dispatched twice through the event system.
          // TODO: If this is the first discrete event in the queue. Schedule an increased
          // priority for this boundary.
          return instance;
        }
        // This shouldn't happen, something went wrong but to avoid blocking
        // the whole system, dispatch the event without a target.
        // TODO: Warn.
        targetInst = null;
      } else if (tag === HostRoot) {
        const root: FiberRoot = nearestMounted.stateNode;
        if (root.hydrate) {
          // If this happens during a replay something went wrong and it might block
          // the whole system.
          return getContainerFromFiber(nearestMounted);
        }
        targetInst = null;
      } else if (nearestMounted !== targetInst) {
        // If we get an event (ex: img onload) before committing that
        // component's mount, ignore it for now (that is, treat it as if it was an
        // event on a non-React tree). We might also consider queueing events and
        // dispatching them after the mount.
        targetInst = null;
      }
    }
  }
  // 进行相关事件任务的调度
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  // We're not blocked on anything.
  return null;
}

dispatchEventForPluginEventSystem()

其主要作用是:

  1. 处理目标元素为Potal类型组件的事件
  2. 按照批量更新的方式 调度 dispatchEventsForPlugins
js
/**
 * 核心任务是
 *  1. 处理Potal类型组件的事件
 *  2. 按照批量更新的方式 调度 dispatchEventsForPlugins
 *
 * @param {*} domEventName
 * @param {*} eventSystemFlags
 * @param {*} nativeEvent
 * @param {*} targetInst
 * @param {*} targetContainer
 * @returns
 */
export function dispatchEventForPluginEventSystem(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  let ancestorInst = targetInst;
  if (
    (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
    (eventSystemFlags & IS_NON_DELEGATED) === 0
  ) {
    const targetContainerNode = ((targetContainer: any): Node);

    // If we are using the legacy FB support flag, we
    // defer the event to the null with a one
    // time event listener so we can defer the event.
    if (
      enableLegacyFBSupport &&
      // If our event flags match the required flags for entering
      // FB legacy mode and we are prcocessing the "click" event,
      // then we can defer the event to the "document", to allow
      // for legacy FB support, where the expected behavior was to
      // match React < 16 behavior of delegated clicks to the doc.
      domEventName === 'click' &&
      (eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
    ) {
      deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
      return;
    }
    if (targetInst !== null) {
      let node = targetInst;

      mainLoop: while (true) {
        if (node === null) {
          return;
        }
        const nodeTag = node.tag;
        if (nodeTag === HostRoot || nodeTag === HostPortal) {
          let container = node.stateNode.containerInfo;
          if (isMatchingRootContainer(container, targetContainerNode)) {
            break;
          }
          if (nodeTag === HostPortal) {
            // The target is a portal, but it's not the rootContainer we're looking for.
            // Normally portals handle their own events all the way down to the root.
            // So we should be able to stop now. However, we don't know if this portal
            // was part of *our* root.
            let grandNode = node.return;
            while (grandNode !== null) {
              const grandTag = grandNode.tag;
              if (grandTag === HostRoot || grandTag === HostPortal) {
                const grandContainer = grandNode.stateNode.containerInfo;
                if (
                  isMatchingRootContainer(grandContainer, targetContainerNode)
                ) {
                  // This is the rootContainer we're looking for and we found it as
                  // a parent of the Portal. That means we can ignore it because the
                  // Portal will bubble through to us.
                  return;
                }
              }
              grandNode = grandNode.return;
            }
          }
          // Now we need to find it's corresponding host fiber in the other
          // tree. To do this we can use getClosestInstanceFromNode, but we
          // need to validate that the fiber is a host instance, otherwise
          // we need to traverse up through the DOM till we find the correct
          // node that is from the other tree.
          while (container !== null) {
            const parentNode = getClosestInstanceFromNode(container);
            if (parentNode === null) {
              return;
            }
            const parentTag = parentNode.tag;
            if (parentTag === HostComponent || parentTag === HostText) {
              node = ancestorInst = parentNode;
              continue mainLoop;
            }
            container = container.parentNode;
          }
        }
        node = node.return;
      }
    }
  }

  batchedEventUpdates(() =>
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst,
      targetContainer,
    ),
  );
}

dispatchEventsForPlugins()

之前的方法都是对依赖对象的处理和判断,这个才是事件触发的核心过程

js
/**
 * 真正的调度任务了
 * @param {} domEventName
 * @param {*} eventSystemFlags
 * @param {*} nativeEvent
 * @param {*} targetInst
 * @param {*} targetContainer
 */
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  // 获取target
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 存放 事件调度任务的队列 -
  const dispatchQueue: DispatchQueue = [];
  // 获取目标对象到root链路上注册的事件,并保存到dispatchQueue队列中
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );

  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents()

这个就涉及到事件的插件机制

其主要流程是:

  1. 在插件初始化的时候会通过高阶函数的方式生成不同类型的事件其event实例对象构造方法

    • SyntheticMouseEvent

    • SyntheticDragEvent

    • SyntheticKeyboardEvent

    • SyntheticPointerEvent

    • SyntheticTouchEvent

    • SyntheticTransitionEvent

    • SyntheticWheelEvent

    • ...

  2. 根据事件的名称获取其对应的event实例对象构造方法

  3. 通过向上遍历的方式生成从targetInst到Root这个链上注册事件的 listener 对象

  4. 实例化一个SyntheticEventCtor 对象(这条链上事件回调函数公用)

  5. 生成DispatchQueue

合成事件对象构造方法

我们先简单看一下 mouse类型的事件其构造方法为通过 createSyntheticEvent(MouseEventInterface) 生成,其核心是通过createSyntheticEvent()中的SyntheticBaseEvent去生成所有的事件共有的属性和方法,然后通过不同的事件类型提供的interface去拓展各自的事件实例对象 SyntheticEvent

我们看一下SyntheticBaseEvent()

js
 /**
   * 所有合成事件Event对象的基类
   * @param {*} reactName       事件在React定义的名称 如 onClick
   * @param {*} reactEventType  事件的原生类型           click
   * @param {*} targetInst      事件的目标元素的FiberNode对象
   * @param {*} nativeEvent     事件的目标元素的 原生 事件对象
   * @param {*} nativeEventTarget 事件目标元素的 原生 e.target 对象
   * @returns
   */
  function SyntheticBaseEvent(
    reactName: string | null,
    reactEventType: string,
    targetInst: Fiber,
    nativeEvent: {[propName: string]: mixed},
    nativeEventTarget: null | EventTarget,
  ) {
    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;
      }
      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;
  }

  Object.assign(SyntheticBaseEvent.prototype, {
    // 提供统一的合成事件的 阻止默认行为的方法
    preventDefault: function() {
      this.defaultPrevented = true;
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.preventDefault) {
        event.preventDefault();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.returnValue !== 'unknown') {
        event.returnValue = false;
      }
      this.isDefaultPrevented = functionThatReturnsTrue;
    },
    // 提供统一的合成事件的 阻止冒泡或者捕获的方法
    stopPropagation: function() {
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.stopPropagation) {
        event.stopPropagation();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.cancelBubble !== 'unknown') {
        event.cancelBubble = true;
      }
      this.isPropagationStopped = functionThatReturnsTrue;
    },

    persist: function() {
      // Modern event system doesn't use pooling.
    },
    isPersistent: functionThatReturnsTrue,
  });

我们先看简单类型的事件

简单事件类型
js

/**
 * 简单事件插件的 提取任务对象的方法
 *
 *  1. 根据事件类型获取初始化(高阶函数返回的事件event Class对象)
 *  2. accumulateSinglePhaseListeners
 *      构建从目标对象到root这里链路上注册了此事件的方法
 *  3. 如果有注册事件方法,那么就初始化一个统一的 event实例对象
 *    其存储结构为 { event ,  listeners : [ 事件button(DispatchListener) , 事件div(DispatchListener) ] }
 * @param {*} dispatchQueue
 * @param {*} domEventName
 * @param {*} targetInst
 * @param {*} nativeEvent
 * @param {*} nativeEventTarget
 * @param {*} eventSystemFlags
 * @param {*} targetContainer
 * @returns
 */
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  // 获取事件的名称
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  // 根据事件名称的不同 获取不同的event对象
  switch (domEventName) {
    case 'keypress':
      // Firefox creates a keypress event for function keys too. This removes
      // the unwanted keypress events. Enter is however both printable and
      // non-printable. One would expect Tab to be as well (but it isn't).
      if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
        return;
      }
    /* falls through */
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent;
      break;
    case 'focusin':
      reactEventType = 'focus';
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case 'focusout':
      reactEventType = 'blur';
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case 'beforeblur':
    case 'afterblur':
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case 'click':
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return;
      }
    /* falls through */
    case 'auxclick':
    case 'dblclick':
    case 'mousedown':
    case 'mousemove':
    case 'mouseup':
    // TODO: Disabled elements should not respond to mouse events
    /* falls through */
    case 'mouseout':
    case 'mouseover':
    case 'contextmenu':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case 'drag':
    case 'dragend':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'dragstart':
    case 'drop':
      SyntheticEventCtor = SyntheticDragEvent;
      break;
    case 'touchcancel':
    case 'touchend':
    case 'touchmove':
    case 'touchstart':
      SyntheticEventCtor = SyntheticTouchEvent;
      break;
    case ANIMATION_END:
    case ANIMATION_ITERATION:
    case ANIMATION_START:
      SyntheticEventCtor = SyntheticAnimationEvent;
      break;
    case TRANSITION_END:
      SyntheticEventCtor = SyntheticTransitionEvent;
      break;
    case 'scroll':
      SyntheticEventCtor = SyntheticUIEvent;
      break;
    case 'wheel':
      SyntheticEventCtor = SyntheticWheelEvent;
      break;
    case 'copy':
    case 'cut':
    case 'paste':
      SyntheticEventCtor = SyntheticClipboardEvent;
      break;
    case 'gotpointercapture':
    case 'lostpointercapture':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'pointerup':
      SyntheticEventCtor = SyntheticPointerEvent;
      break;
    default:
      // Unknown event. This is used by createEventHandle.
      break;
  }

  // 是否是捕获阶段触发的
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {

    // 获取目标对象及其祖先中注册了此类型事件的 listeners 数组
    const listeners = accumulateEventHandleNonManagedNodeListeners(
      // TODO: this cast may not make sense for events like
      // "focus" where React listens to e.g. "focusin".
      ((reactEventType: any): DOMEventName),
      targetContainer,
      inCapturePhase,
    );
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      // 创建一个 事件实例对象
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      // 保存到 调度队列中
      dispatchQueue.push({event, listeners});
    }
  } else {
    // Some events don't bubble in the browser.
    // In the past, React has always bubbled them, but this can be surprising.
    // We're going to try aligning closer to the browser behavior by not bubbling
    // them in React either. We'll start by not bubbling onScroll, and then expand.
    const accumulateTargetOnly =
      !inCapturePhase &&
      // TODO: ideally, we'd eventually add all events from
      // nonDelegatedEvents list in DOMPluginEventSystem.
      // Then we can remove this special list.
      // This is a breaking change that can wait until React 18.
      domEventName === 'scroll';
    // 通过向上遍历的方式 去构建整个事件目标链路上的 委托的事件listeneres
    const listeners = accumulateSinglePhaseListeners(
      targetInst,
      reactName,
      nativeEvent.type,
      inCapturePhase,
      accumulateTargetOnly,
    );
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      // 生成一个公共的 event 对象
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      dispatchQueue.push({event, listeners});
    }
  }
}

React事件DispatchQueue对象

对于一次事件的触发,其可以在事件冒泡或者事件捕获阶段回调好几个事件函数,那么其怎么维护原生事件流中事件从捕获到冒泡的触发机制的,这就涉及到React合成事件的DispatchQueue对象和 processDispatchQueue 派发任务执行机制

下面我们分别看如何生成DispatchQueue对象 和 任务如何进行派发执行的

  1. DispatchQueue对象

accumulateSinglePhaseListeners()

从源码中可见其通过 fiberNode.return去向上遍历,找到这条链路上绑定了这个事件的属性,然后通过createDispatchListener(instance, listener, lastHostComponent) 构建了一个包含

  • instance FiberNode实例对象
  • listener事件的回调函数
  • lastHostComponent 原生DOM数据

的listener对象

js
/**
 * 通过向上遍历的方式 去构建整个事件目标链路上的 委托的事件listeneres
 * @param {*} targetFiber
 * @param {*} reactName
 * @param {*} nativeEventType
 * @param {*} inCapturePhase
 * @param {*} accumulateTargetOnly
 * @returns DispatchListener[]
 */
export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
): Array<DispatchListener> {
  //
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  // 事件的名称
  const reactEventName = inCapturePhase ? captureName : reactName;
  const listeners: Array<DispatchListener> = [];

  let instance = targetFiber;
  let lastHostComponent = null;

  // Accumulate all instances and listeners via the target -> root path.
  while (instance !== null) {
    const {stateNode, tag} = instance;
      // Standard React on* listeners, i.e. onClick or onClickCapture
      if (reactEventName !== null) {
        // 获取组件上定义的 事件 属性内容
        // 如: button 组件上 onClick 的内容
        const listener = getListener(instance, reactEventName);
        // 如果存在
        if (listener != null) {
          // 加入到 事件队列中
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent),
          );
        }
      }
    } 
    if (accumulateTargetOnly) {
      break;
    }
    // 向上循环
    instance = instance.return;
  }
  return listeners;
}

然后通过dispatchQueue.push({event, listeners}) 生成了一个当前事件回调队列共享同一个合成事件Event对象的队列。

企业微信截图_1666021647235

React事件派发执行的过程

主要是通过

  1. 捕获类型的事件 从后向前遍历执行
  2. 冒泡类型的事件 从前先后遍历执行

去维护事件冒泡和捕获的执行顺序的

通过同一个事件执行过程生成一个 dispatchQueue并共享一个event对象,在事件回调执行前 currentTarget = currentTarget 和执行后 currentTarget = null 去维护合成事件对象的共享和区分的。

这边还涉及到例外一个问题。

事件的冒泡和事件捕获其实不是一个事件。

如click事件,在捕获阶段其事件名称为onClickCapture 在冒泡阶段才是 onClick

js
/**
 * 回调执行事件
 *  通过将 执行前 currentTarget = currentTarget
 *        执行前 currentTarget = null 
 * 去共享一个合成事件对象
 * @param {*} event
 * @param {*} listener
 * @param {*} currentTarget
 */
function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget,
): void {
  const type = event.type || 'unknown-event';
  // 修改event对象的目标对象为本身
  event.currentTarget = currentTarget;
  // 回调事件方法
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  // 重置 currentTarget
  event.currentTarget = null;
}

/**
 * 派发事件执行的过程
 *  1. 捕获类型的事件 从后向前遍历执行
 *  2. 冒泡类型的事件 从前先后遍历执行
 *  
 * @param {*} event 
 * @param {*} dispatchListeners 
 * @param {*} inCapturePhase 
 * @returns 
 */
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    // 捕获类型的事件 从后向前遍历执行
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      // 获取事件执行 DispatchListener
      const {instance, currentTarget, listener} = dispatchListeners[i];
      // 如果执行了 event.isPropagationStopped() 就停止向下捕获
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    // 冒泡类型的事件 从前先后遍历执行
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      // 如果执行了 event.isPropagationStopped() 就停止向上冒泡
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}