技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# React最新事件机制

React v17 里事件机制有了比较大的改动,想来和 v16 差别还是比较大的。

本文浅析的 React 版本为 17.0.1,使用ReactDOM.render创建应用,不含优先级相关。

# 原理简述

React 事件建立在原生基础上,模拟了一套冒泡和捕获的事件机制,当某一个 DOM 元素触发事件后,会冒泡到 React 绑定在root节点的处理函数,通过target获取触发事件的 DOM 对象和对应的 Fiber 节点,由该 Fiber 节点向上层父级遍历,收集一条事件队列,再遍历该队列触发队列中每个 Fiber 对象对应的事件处理函数,正向遍历模拟冒泡,反向遍历模拟捕获,所以合成事件的触发时机是在原生事件之后的。

Fiber 对象对应的事件处理函数依旧是储存在props里的,收集只是从props里取出来,它并没有绑定到任何元素上。

React 中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的 DOM 元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在 DOM 元素本身。

同时,React 将事件分为 3 种类型/3 个优先级

  • dispatchDiscreteEvent/DiscreteEventPriority:处理离散事件,例如 blur、focus、 click、 submit、 touchStart. 这些事件都是离散触发的

  • dispatchContinuousEvent/ContinuousEventPriority:处理连续事件;例如 load、error、loadStart、abort、animationEnd. 这个优先级最高,也就是说它们应该是立即同步执行的,这就是 Continuous 的意义,即可连续的执行,不被打断

  • dispatchEvent/DefaultEventPriority: 例如 touchMove、mouseMove、scroll、drag、dragOver 等等。这些事件会’阻塞’用户的交互。

它们拥有不同的优先级,在绑定事件处理函数时会使用不同的回调函数。

# React 为什么自定义事件系统

  1. 抹平浏览器之间的兼容性差异。 这是估计最原始的动机,React 根据 W3C 规范来定义这些合成事件(SyntheticEvent), 意在抹平浏览器之间的差异。

另外 React 还会试图通过其他相关事件来模拟一些低版本不兼容的事件, 这才是‘合成’的本来意思吧?。

  1. 事件‘合成’, 即事件自定义。事件合成除了处理兼容性问题,还可以用来自定义高级事件,比较典型的是 ReactonChange 事件,它为表单元素定义了统一的值变动事件。另外第三方也可以通过 React 的事件插件机制来合成自定义事件,尽管很少人这么做。
// onChange事件是由change,click,focusin,input,selectionchange等组合而成
function registerEvents() {
  registerTwoPhaseEvent('onChange', [
    'change',
    'click',
    'focusin',
    'focusout',
    'input',
    'keydown',
    'keyup',
    'selectionchange',
  ]);
}
  1. 抽象跨平台事件机制。 和 VirtualDOM 的意义差不多,VirtualDOM 抽象了跨平台的渲染方式,那么对应的 SyntheticEvent 目的也是想提供一个抽象的跨平台事件机制。

  2. React 打算做更多优化。比如利用事件委托机制,大部分事件最终绑定到了 根root上, 这样简化了 DOM 事件处理逻辑,减少了内存开销. 但这也意味着,React 需要自己模拟一套事件冒泡的机制。

  3. React 打算干预事件的分发。v16 引入 Fiber 架构,React 为了优化用户的交互体验,会干预事件的分发。不同类型的事件有不同的优先级,比如高优先级的事件可以中断渲染,让用户代码可以及时响应用户交互。

# 源码浅析

以下源码仅为基础逻辑的浅析,旨在理清事件机制的触发流程,去掉了很多流程无关或复杂的代码。

# 委托事件绑定

这一步发生在调用了ReactDOM.render过程中,在创建fiberRoot的时候会在root节点的 DOM 元素上监听所有支持的事件。

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions
) {
  // ...
  const rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
  // 监听所有支持的事件
  listenToAllSupportedEvents(rootContainerElement);
  // ...
}

# listenToAllSupportedEvents

在绑定事件时,会通过名为allNativeEventsSet变量来获取对应的eventName,这个变量会在一个顶层函数进行收集,而nonDelegatedEvents是一个预先定义好的Set

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  allNativeEvents.forEach((domEventName) => {
    // 排除不需要委托的事件
    if (!nonDelegatedEvents.has(domEventName)) {
      // 冒泡
      listenToNativeEvent(
        domEventName,
        false,
        ((rootContainerElement: any): Element),
        null
      );
    }
    // 捕获
    listenToNativeEvent(
      domEventName,
      true,
      ((rootContainerElement: any): Element),
      null
    );
  });
}

# listenToNativeEvent

listenToNativeEvent函数在绑定事件之前会先将事件名在 DOM 元素中标记,判断为false时才会绑定。

export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  rootContainerElement: EventTarget,
  targetElement: Element | null,
  eventSystemFlags?: EventSystemFlags = 0
): void {
  let target = rootContainerElement;
  // ...
  // 在DOM元素上储存一个Set用来标识当前元素监听了那些事件
  const listenerSet = getEventListenerSet(target);
  // 事件的标识key,字符串拼接处理了下
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener
  );

  if (!listenerSet.has(listenerSetKey)) {
    // 标记为捕获
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    // 绑定事件
    addTrappedEventListener(
      target,
      domEventName,
      eventSystemFlags,
      isCapturePhaseListener
    );
    // 添加到set
    listenerSet.add(listenerSetKey);
  }
}

# addTrappedEventListener

addTrappedEventListener函数会通过事件名取得对应优先级的listener函数,在交由下层函数处理事件绑定。

这个listener函数是一个闭包函数,函数内能访问targetContainerdomEventNameeventSystemFlags这三个变量。

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean
) {
  // 根据优先级取得对应listener
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  );

  if (isCapturePhaseListener) {
    addEventCaptureListener(targetContainer, domEventName, listener);
  } else {
    addEventBubbleListener(targetContainer, domEventName, listener);
  }
}

addEventCaptureListener函数和addEventBubbleListener函数内部就是调用原生的target.addEventListener来绑定事件了。


这一步是循环一个存有事件名的Set,将每一个事件对应的处理函数绑定到root节点 DOM 元素上。

# 不需要委托事件绑定

不需要委托的事件其中也包括媒体元素的事件。

export const nonDelegatedEvents: Set<DOMEventName> = new Set([
  "cancel",
  "close",
  "invalid",
  "load",
  "scroll",
  "toggle",
  ...mediaEventTypes,
]);
export const mediaEventTypes: Array<DOMEventName> = [
  "abort",
  "canplay",
  "canplaythrough",
  "durationchange",
  "emptied",
  "encrypted",
  "ended",
  "error",
  "loadeddata",
  "loadedmetadata",
  "loadstart",
  "pause",
  "play",
  "playing",
  "progress",
  "ratechange",
  "seeked",
  "seeking",
  "stalled",
  "suspend",
  "timeupdate",
  "volumechange",
  "waiting",
];

这些事件绑定发生在completeWork阶段,最后会执行setInitialProperties方法。

# setInitialProperties

setInitialProperties方法里会绑定不需要委托的直接到 DOM 元素本身,也会设置style和一些传入的 DOM 属性。

export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document
): void {
  let props: Object;
  switch (tag) {
    // ...
    case "video":
    case "audio":
      for (let i = 0; i < mediaEventTypes.length; i++) {
        listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
      }
      props = rawProps;
      break;
    default:
      props = rawProps;
  }
  // 设置DOM属性,如style...
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag
  );
}

switch里会根据不同的元素类型,绑定对应的事件,这里只留下了video元素和audio元素的处理,它们会遍历mediaEventTypes来将事件绑定在 DOM 元素本身上。

# listenToNonDelegatedEvent

listenToNonDelegatedEvent方法逻辑和上一节的listenToNativeEvent方法基本一致。

export function listenToNonDelegatedEvent(
  domEventName: DOMEventName,
  targetElement: Element
): void {
  const isCapturePhaseListener = false;
  const listenerSet = getEventListenerSet(targetElement);
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener
  );
  if (!listenerSet.has(listenerSetKey)) {
    addTrappedEventListener(
      targetElement,
      domEventName,
      IS_NON_DELEGATED,
      isCapturePhaseListener
    );
    listenerSet.add(listenerSetKey);
  }
}

值得注意的是,虽然事件处理绑定在 DOM 元素本身,但是绑定的事件处理函数不是代码中传入的函数,后续触发还是会去收集处理函数执行。

# 事件处理函数

事件处理函数指的是 React 中的默认处理函数,并不是代码里传入的函数。

这个函数通过createEventListenerWrapperWithPriority方法创建,对应的步骤在上文的addTrappedEventListener中。

# createEventListenerWrapperWithPriority

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags
): Function {
  // 从内置的Map中获取事件优先级
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  // 根据优先级不同返回不同的listener
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer
  );
}

createEventListenerWrapperWithPriority函数里返回对应事件优先级的listener,这 3 个函数都接收 4 个参数。

function fn(domEventName, eventSystemFlags, container, nativeEvent) {
  //...
}

返回的时候bind了一下传入了 3 个参数,这样返回的函数为只接收nativeEvent的处理函数了,但是能访问前 3 个参数。

dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法内部其实都调用的dispatchEvent方法。

# dispatchEvent

这里删除了很多代码,只看触发事件的代码。

export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
): void {
  // ...
  // 触发事件
  attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent
  );
  // ...
}

attemptToDispatchEvent方法里依然会处理很多复杂逻辑,同时函数调用栈也有几层,我们就全部跳过,只看关键的触发函数。

# dispatchEventsForPlugins

dispatchEventsForPlugins函数里会收集触发事件开始各层级的节点对应的处理函数,也就是我们实际传入JSX中的函数,并且执行它们。

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  // 收集listener模拟冒泡
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );
  // 执行队列
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

# extractEvents

extractEvents函数里主要是针对不同类型的事件创建对应的合成事件,并且将各层级节点的listener收集起来,用来模拟冒泡或者捕获。

这里的代码较长,删除了不少无关代码。

function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  // 根据不同的事件来创建不同的合成事件
  switch (domEventName) {
    case "keypress":
    case "keydown":
    case "keyup":
      SyntheticEventCtor = SyntheticKeyboardEvent;
      break;
    case "click":
    // ...
    case "mouseover":
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case "drag":
    // ...
    case "drop":
      SyntheticEventCtor = SyntheticDragEvent;
      break;
    // ...
    default:
      break;
  }
  // ...
  // 收集各层级的listener
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly
  );
  if (listeners.length > 0) {
    // 创建合成事件
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget
    );
    dispatchQueue.push({ event, listeners });
  }
}

# accumulateSinglePhaseListeners

accumulateSinglePhaseListeners函数里就是在向上层遍历来收集一个列表后面会用来模拟冒泡。

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;

  // 通过触发事件的fiber节点向上层遍历收集dom和listener
  while (instance !== null) {
    const { stateNode, tag } = instance;
    // 只有HostComponents有listener (i.e. <div>)
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode;

      if (reactEventName !== null) {
        // 从fiber节点上的props中获取传入的事件listener函数
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push({
            instance,
            listener,
            currentTarget: lastHostComponent,
          });
        }
      }
    }
    if (accumulateTargetOnly) {
      break;
    }
    // 继续向上
    instance = instance.return;
  }
  return listeners;
}

最后的数据结构如下:

dispatchQueue的数据结构为数组,类型为[{ event,listeners }]

这个listeners则为一层一层收集到的数据,类型为[{ currentTarget, instance, listener }]

# processDispatchQueue

processDispatchQueue函数里会遍历dispatchQueue

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);
  }
}

dispatchQueue中的每一项在processDispatchQueueItemsInOrder函数里遍历执行。

# processDispatchQueueItemsInOrder

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;
    }
  }
}

processDispatchQueueItemsInOrder函数里会根据判断来正向、反向的遍历来模拟冒泡和捕获。

# executeDispatch

executeDispatch函数里会执行listener

function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget
): void {
  const type = event.type || "unknown-event";
  event.currentTarget = currentTarget;
  listener(event);
  event.currentTarget = null;
}

# 总结

React 提供了一种顶层注册,事件收集,统一触发的事件机制。

  • 所谓“顶层注册”,其实是在 root 元素上绑定一个统一的事件处理函数。
  • “事件收集”指的是事件触发时(实际上是 root 上的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数。
  • “统一触发”发生在收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象

# 小思考

# 1、为什么原生事件的stopPropagation可以阻止合成事件的传递?

  • 因为合成事件是在原生事件触发之后才开始收集并触发的,所以当原生事件调用stopPropagation阻止传递后,根本到不到root节点,触发不了 React 绑定的处理函数,自然合成事件也不会触发,所以原生事件不是阻止了合成事件的传递,而是阻止了 React 中绑定的事件函数的执行。

    <div 原生onClick={(e)=>{e.stopPropagation()}}>
      <div onClick={()=>{console.log("合成事件")}}>合成事件</div>
    </div>
    
    

    比如这个例子,在原生 onClick 阻止传递后,控制台连“合成事件”这 4 个字都不会打出来了。

# 2、为什么要手动绑定 this

通过事件触发过程的分析,processDispatchQueue->processDispatchQueueItemsInOrder->executeDispatch调用了invokeGuardedCallback方法。

export function invokeGuardedCallbackAndCatchFirstError<
  A,
  B,
  C,
  D,
  E,
  F,
  Context
>(
  name: string | null,
  func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
  context: Context,
  a: A,
  b: B,
  c: C,
  d: D,
  e: E,
  f: F
): void {
  invokeGuardedCallback.apply(this, arguments);
  if (hasError) {
    const error = clearCaughtError();
    if (!hasRethrowError) {
      hasRethrowError = true;
      rethrowError = error;
    }
  }
}
const reporter = {
  onError(error: mixed) {
    hasError = true;
    caughtError = error;
  },
};

export function invokeGuardedCallback<A, B, C, D, E, F, Context>(
  name: string | null,
  func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
  context: Context,
  a: A,
  b: B,
  c: C,
  d: D,
  e: E,
  f: F
): void {
  hasError = false;
  caughtError = null;
  invokeGuardedCallbackImpl.apply(reporter, arguments);
}

可见,回调函数是直接调用调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的thisreporter

这里可以使用实验性的属性初始化语法 (opens new window) ,也就是直接在组件声明箭头函数。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此这样我们在React事件中获取到的就是组件本身了。

# 3、和原生事件有什么区别

  • React 事件使用驼峰命名,而不是全部小写。

  • 通过 JSX , 你传递一个函数作为事件处理程序,而不是一个字符串。

例如,HTML

<button onclick="activateLasers()">Activate Lasers</button>

React 中略有不同:

<button onClick={activateLasers}>Activate Lasers</button>

另一个区别是,在 React 中你不能通过返回false 来阻止默认行为。必须明确调用 preventDefault

由上面执行机制我们可以得出:React自己实现了一套事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,并且抹平了各个浏览器的兼容性问题。

# 4、react 事件和原生事件可以混用吗?

react事件和原生事件最好不要混用。

原生事件中如果执行了stopPropagation方法,则会导致其他react事件失效。因为所有元素的事件将无法冒泡到rootNode上。

由上面的执行机制不难得出,所有的 react 事件都将无法被注册。

# 5、合成事件、浏览器兼容

function handleClick(e) {
  e.preventDefault();
  console.log("The link was clicked.");
}

这里, e 是一个合成的事件。 React 根据 W3C (opens new window) 规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。

事件处理程序将传递 SyntheticEvent 的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括stopPropagation()preventDefault() ,在所有浏览器中他们工作方式都相同。

# 参考

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM