creamidea / creamidea.github.com

冰糖火箭筒&&蜂蜜甜甜圈
https://creamidea.github.io/
4 stars 4 forks source link

React 事件处理 #47

Open creamidea opened 2 years ago

creamidea commented 2 years ago

事件绑定

从 createRoot 函数开始

function createRoot(container) {
  ...

  const rootContainerElement: Document | Element | DocumentFragment =
  container.nodeType === COMMENT_NODE
    ? (container.parentNode: any)
    : container;
  listenToAllSupportedEvents(rootContainerElement);

  ...
}

listenToAllSupportedEvents 进行原生事件绑定。也会处理 nonDelegatedEvents,selectionchange 事件等

function listenToAllSupportedEvents() {
  // 核心代码,listenToNativeEvent 调用 addTrappedEventListener
  // eventSystemFlags |= IS_CAPTURE_PHASE;
  listenToNativeEvent(domEventName, true, rootContainerElement);
}

addTrappedEventListener 函数根据不同事件,构造不同优先级的处理函数(createEventListenerWrapperWithPriority),得到该函数之后进行原生 DOM 事件绑定,绑定到根 DOM 节点。比如,click 事件,处理函数就是 dispatchDiscreteEvent,默认是 dispatchEvent。并通过 bind 的方式,固定参数:domEventName、eventSystemFlags、targetContainer(即 root dom 节点实例)

至此,绑定原生事件的过程结束,后续就是等待用户交互,触发原生事件,进入诸如 dispatchEvent 函数进行处理。

事件处理

进入诸如 dispatchEvent 函数进行处理。其最总调用 dispatchEventOriginal 函数处理。

function dispatchEventOriginal() {
  ...
  const blockedOn = findInstanceBlockingEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent,
  );
  if (blockedOn === null) {
    dispatchEventForPluginEventSystem(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      return_targetInst,
      targetContainer,
    );
    ...
    return;
  }
  ...
}

function findInstanceBlockingEvent() {
  // 该值会在函数中处理,有值的 2 种情况
  // - targetInst NearestMountedFiber 是 Suspense
  // - Hydrating 情况下,targetInst NearestMountedFiber 是 HostRoot
  return_targetInst = null;

  const nativeEventTarget = getEventTarget(nativeEvent);

  // targetInst 是根据当前事件的 DOM 节点中存储的 internalInstanceKey 找到对应 fiber 实例
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  ...

  return null
}

blockedOn 有值的 2 种情况,有值则会调用 queueDiscreteEvent 放入到 queuedDiscreteEvents 队列(retryIfBlockedOn 会消费)

function dispatchEventForPluginEventSystem() {
  // 注意,这里的 targetInst 是👆说的 return_targetInst 这个全局变量
  // 即处理 Suspense 或者 HostRoot 情况
  if (targetInst !== null) {
    ...
  }

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

dispatchEventsForPlugins 函数就是调用注册的 Plugin 的 extract 方法,提取事件。比如 SimpleEventPlugin.extractEvents 函数内会通过 accumulateSinglePhaseListeners 函数收集绑定的事件,并构造 SyntheticEvent,一起放入到 dispatchQueue 队列

function extractEvents() {
  ...
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent,
  );
  if (listeners.length > 0) {
    // Intentionally create event lazily.
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget,
    );
    dispatchQueue.push({event, listeners});
  }
  ...
}

function accumulateSinglePhaseListeners() {
  // Accumulate all instances and listeners via the target -> root path.
}

dispatchQueue 队列会在 processDispatchQueue 函数内被处理

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;

  if (inCapturePhase) {
    // 处理捕获事件
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      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];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}
creamidea commented 2 years ago

listenToAllSupportedEvents 会把所有的事件都在 container 上注册一遍!!!

dispatchEventForPluginEventSystem(
  domEventName, // 原生事件名称
  eventSystemFlags, // 事件标志,比如是否 capture
  nativeEvent, // 原生事件对象
  return_targetInst, // null, Suspense, HostRoot。后面也被定义为 ancestorInst
  targetContainer, // 根 DOM 容器
);

onSelect, onChange 这里事件,会由 SelectEventPlugin.js, ChangeEventPlugin.js 内注册处理。原理:会事先通过 registerEvents 函数注册

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

在 extract 阶段,会捕获原生 DOM 事件,比如 mousedown,那么进入 switch-case 进行处理,转成查找注册给 React onSelect 事件

function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) {
  ...
  const listeners = accumulateTwoPhaseListeners(
    activeElementInst,
    'onSelect',
  );
  ...
}
creamidea commented 2 years ago

https://stackoverflow.com/questions/44279434/react-onselect-event-not-working