ZHANGYU.dev

October 14, 2023

React事件机制源码浅析

React12.9 min to read

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

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

原理简述

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

同时,React将事件分为3种类型——discreteEventuserBlockingEventcontinuousEvent,它们拥有不同的优先级,在绑定事件处理函数时会使用不同的回调函数。

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

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

源码浅析

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

委托事件绑定

这一步发生在调用了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的事件机制隐晦而复杂,根据不同情况做了非常多的判断,并且还有优先级相关代码、合成事件,这里都没有一一讲解,原因当然是我还没看~

平时用React也就写写简单的手机页面,以前老板还经常吐槽加载不够快,那也没啥办法,就对我的工作而言,有没有Cocurrent都是无关紧要的,这合成事件更复杂,完全就是不需要的,不过React的作者们脑洞还是牛皮,要是没看源码我肯定是想不到竟然模拟了一套事件机制。

小思考

这些问题我放以前根本没想过,不过今天看了源码以后才想的。