源码不害怕 - 拆解 React 合成事件源码

tip: 源码部分基于React18

1.简单介绍下Fiber

概述

Fiber 是 React 中一种用于实现虚拟 DOM 以及实现协调更新的架构。Fiber是一种架构、数据结构、也是一种工作单元;

Demo介绍Fiber树:

JSX结构:

js 复制代码
    <div> 
        <ul> 
            <li>li1</li> 
            <li>li2</li> 
        </ul> 
        <button>
            button
        </button> 
    <div>

Dom树结构:

Fiber树结构:

父节点只和它的第一个孩子节点相连接,而第一个孩子和后面的兄弟节点相连接,它们之间构成了一个单项链表的结构。最后,每个孩子都有一个指向父节点的指针

js 复制代码
export type Fiber = {
  // Tag identifying the type of fiber
  // 标记不同组件类型,如classComponent,functionComponent
  tag: WorkTag,

  // Unique identifier of this child.
  key: null | string,

  // The value of element.type which is used to preserve the identity during
  // reconciliation of this child.
  
  elementType: any,


  // The resolved function/class/ associated with this fiber.
  //描述了它对应的组件,对于复合组件,type 是函数或类组件本身。对于原生标签(div,span等),
  //type 是一个字符串
  type: any,

  // The local state associated with this fiber.
  //实例对象 真实dom节点
  stateNode: any,
  
  // 指针 父、子、兄弟节点
  return: Fiber | null,
  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  // The ref last used to attach this node.
  // I'll avoid adding an owner field for prod and model that as functions.
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,

  // Input is the data coming into process this fiber. Arguments. Props.
  pendingProps: any, // This type will be more specific once we overload the tag.
  memoizedProps: any, // The props used to create the output.

  // A queue of state updates and callbacks.
  updateQueue: mixed,

  // The state used to create the output
  memoizedState: any,

  // Dependencies (contexts, events) for this fiber, if it has any
  dependencies: Dependencies | null,

  mode: TypeOfMode,

  // Effect 副作用
  flags: Flags,
  subtreeFlags: Flags,
  deletions: Array<Fiber> | null,

  // Singly linked list fast path to the next fiber with side-effects.
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  // Priority 优先级
  lanes: Lanes,
  childLanes: Lanes,
  
  alternate: Fiber | null,


  actualDuration?: number,
  actualStartTime?: number,
  selfBaseDuration?: number,

  treeBaseDuration?: number,

  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,

  // Used to verify that the order of hooks does not change between renders.
  _debugHookTypes?: Array<HookType> | null,
|};

小结:

  1. element对象就是我们的jsx代码,上面保存了propskeychildren等信息
  2. DOM元素就是最终呈现给用户展示的效果
  3. fiber : element 与 Dom元素的桥梁,只要elemnet发生改变,就会通过fiber做一次调和,使对应的DOM元素发生改变

2.事件机制

(DOM)事件机制包含以下几个概念:

  • 事件类型(Event Types) :浏览器支持多种类型的事件,如鼠标事件(click、mousemove、mousedown、mouseup 等)、键盘事件(keydown、keyup 等)、表单事件(submit、change 等)等。
  • 事件监听(Event Listening) :通过 JavaScript 可以为特定的 DOM 元素注册事件监听器(事件处理函数),以便在事件发生时执行相应的操作。
  • 事件对象(Event Object) :每个事件都关联一个事件对象,包含有关事件的详细信息,如事件类型、触发的 DOM 元素、鼠标位置等。
  • 事件冒泡(Event Bubbling) :当一个事件在 DOM 中触发时,它会从触发元素开始,逐级向上传播,直到根节点。这种冒泡机制允许从更高层次的元素捕获和处理事件。
  • 事件捕获(Event Capturing) :与事件冒泡相反,事件捕获是从根节点开始,逐级向下传播到触发元素。这是一个少用的机制,一般情况下使用冒泡来处理事件。
  • 事件委托(Event Delegation) :事件委托是一种优化技术,通过将事件处理程序绑定到其父元素上,来处理一组子元素上的事件。这样可以减少事件处理程序的数量,提高性能。
js 复制代码
<ul id="parent-list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<script>
const parentList = document.getElementById('parent-list');

parentList.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('Clicked on:', event.target.textContent);
  }
});
</script>
事件委托的优势在于,不必为每个子元素都添加事件监听器,而是通过冒泡阶段捕获事件,
然后在父元素上根据事件的目标进行适当的处理

3.合成事件

合成事件其实就是react自己实现(模拟)一套事件机制,一套更符合fiber体质的事件机制; 我们在jsx上写的onClick并不是直接被绑定到真实的DOM节点上,因为生成fiber节点的时候对应的dom节点可能还未挂载,所以onClick其实是作为fiber节点的prop。

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

  • 顶层注册:是在document上绑定一个统一的事件处理函数(React17之后事件是注册到root上而非document),此函数并非我们写在组件中的事件处理函数。
  • 事件收集:事件触发时(实际上是上述被绑定的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数。
  • 统一触发:发生在收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象。

下边我们从源码的角度,过一遍合成事件的整体流程: 事件注册 + 事件触发

事件注册

createRoot

js 复制代码
import App from './App';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

React会把所有的浏览器事件绑定到传入的container(我们传入的是root)上面
js 复制代码
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions
): RootType {
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks
  );
  //调用 createFiberRoot 创建应用唯一的一个FiberRoot
  markContainerAsRoot(root.current, container); 
  // 把container这个DOM打上React的标记,就是在DOM上加个属性
  listenToAllSupportedEvents(rootContainerElement);
  return new ReactDOMRoot(root);
}

-   创建FiberRootNode对象
-   创建第一个FiberNode对象,并挂在FiberRootNode对象的current上,
    其自身的stateNode指向该FiberRootNode对象
-   初始化事件监听
-   创建ReactDOMRoot对象并返回

listenToAllSupportedEvents

js 复制代码
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    allNativeEvents.forEach(domEventName => {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      if (domEventName !== 'selectionchange') {
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        // 不许要冒泡,只在冒泡阶段触发的事件
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    const ownerDocument =
      (rootContainerElement: any).nodeType === DOCUMENT_NODE
        ? rootContainerElement
        : (rootContainerElement: any).ownerDocument;
    if (ownerDocument !== null) {
      // The selectionchange event also needs deduplication
      // but it is attached to the document.
      // 注册至 document 
      if (!(ownerDocument: any)[listeningMarker]) {
        (ownerDocument: any)[listeningMarker] = true;
        listenToNativeEvent('selectionchange', false, ownerDocument);
      }
    }
  }
}

// We should not delegate these events to the container, but rather
// set them on the actual target element itself. This is primarily
// because these events do not consistently bubble in the DOM.
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
  'cancel',
  'close',
  'invalid',
  'load',
  'scroll',
  'toggle',
  // In order to reduce bytes, we insert the above array of media events
  // into this Set. Note: the "error" event isn't an exclusive media event,
  // and can occur on other elements too. Rather than duplicate that event,
  // we just take it from the media events array.
  ...mediaEventTypes,
]);

通过循环事件名,根据事件是否冒泡调用listenToNativeEvent(事件名,是否冒泡,绑定的元素)
把除了selectionchange的其他事件事件注到container上,selectionchange会注册在document上
js 复制代码
//所有react事件
const simpleEventPluginEvents = [
  'abort',
  'auxClick',
  'cancel',
  'canPlay',
  'canPlayThrough',
  'click',
  'close',
  'contextMenu',
  'copy',
  'cut',
  'drag',
  'dragEnd',
  'dragEnter',
   ...,
   'wheel',
];
//所有的原生事件
export const allNativeEvents: Set<DOMEventName> = new Set();
//React事件和原生事件的映射 Map 
export const topLevelEventsToReactNames: Map<DOMEventName,string | null> = new Map();
React定义了一个map映射表,把原生事件和react事件相对应,通过调用SimpleEventPlugin.registerEvents实现

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

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

export function registerTwoPhaseEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
): void {
  // 冒泡、捕获
  registerDirectEvent(registrationName, dependencies);
  registerDirectEvent(registrationName + 'Capture', dependencies);
}

export function registerDirectEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
) {
  registrationNameDependencies[registrationName] = dependencies;
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

dependencies 是数组,因为 一个 React 事件可能会对应多个原生事件;
例如,在不同浏览器中,点击事件可能对应于不同的原生事件,如`click`、`mouseup`、`mousedown`等;
React 的合成事件系统的设计目的之一是提供一个统一的跨浏览器事件处理机制,抹平浏览器的差异性。
js 复制代码
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget,
): void {
  
  let eventSystemFlags = 0;
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener,
  );
}

绑定到root上的事件监听不是我们在组件里写的事件处理函数,而是一个持有事件优先级,并能传递事件执行阶段标志的监听器。

js 复制代码
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  //事件包装优先级
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
  
  // If passive option is not supported, then the event will be
  // active and not passive.
  let isPassiveListener = undefined;
  if (passiveBrowserEventsSupported) {
    // Browsers introduced an intervention, making these events
    // passive by default on document. React doesn't bind them
    // to document anymore, but changing this now would undo
    // the performance wins from the change. So we emulate
    // the existing behavior manually on the roots now.
    // https://github.com/facebook/react/issues/19651
    if (
      domEventName === 'touchstart' ||
      domEventName === 'touchmove' ||
      domEventName === 'wheel'
    ) {
      isPassiveListener = true;
    }
  }

  targetContainer =
    enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
      ? (targetContainer: any).ownerDocument
      : targetContainer;

  let unsubscribeListener;
  // TODO: There are too many combinations here. Consolidate them.
  if (isCapturePhaseListener) {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener,
      );
    } else {
      //注册捕捉事件
      unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener,
      );
    }
  } else {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener,
      );
    } else {
      //注册冒泡事件
      unsubscribeListener = addEventBubbleListener(
        targetContainer,
        domEventName,
        listener,
      );
    }
  }
}

export function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, true);
  return listener;
}

createEventListenerWrapperWithPriority

根据事件名称,创建不同优先级的事件监听器

js 复制代码
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  //通过事件名获取该事件的优先级
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  // 根据事件名注册不同的dispatch函数
  switch (eventPriority) {
    case DiscreteEventPriority: //最高优先级sync,第一车道
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority: //第三车道优先级
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:// 最低优先级第五车道
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}
  • 优先级:事件优先级,是根据事件的交互程度划分的。优先级和事件名的映射关系存在于一个Map结构中。

    • 离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。
    • 用户阻塞事件(UserBlockingEvent):drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。
    • 连续事件(ContinuousEvent):canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。
  • 事件监听包装器:真正绑定在root上的就是这个事件监听包装器。不同包装器持有各自的优先级,当对应的事件触发时,调用的其实是这个包含优先级的事件监听。

    • dispatchDiscreteEvent: 处理离散事件
    • dispatchContinuousEvent:处理用户阻塞事件
    • dispatchEvent:处理连续事件
  • 参数eventSystemFlags

    • 事件系统的一个标志,记录事件的各种标记,其中一个标记就是IS_CAPTURE_PHASE,这表明了当前的事件是捕获阶段触发。当事件名含有Capture后缀时,eventSystemFlags会被赋值为IS_CAPTURE_PHASE。
    • 在以优先级创建对应的事件监听时,eventSystemFlags会作为事件监听函数执行时的入参,传递进去。因此,在事件触发的时候就可以知道组件中的事件是以冒泡或是捕获的顺序执行。

事件触发

事件触发 ------ 事件监听器listener做了什么

dispatchContinuousEvent为例看看事件监听器是如何传递优先级、eventSystemFlags的;

js 复制代码
function dispatchContinuousEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {
  const previousPriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = null;
  try {
    setCurrentUpdatePriority(ContinuousEventPriority);
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

ContinuousEventPriority是ContinuousEvent事件的优先级常量,通过setCurrentUpdatePriority设置当前优先级,最后还是调用dispatchEvent => dispatchEventsForPlugins;

dispatchEventsForPlugins

事件监听最终触发的是dispatchEventsForPlugins

js 复制代码
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  // 事件对象的合成,收集事件到执行路径上
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // 执行收集到的组件中真正的事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

dispatchQueue

js 复制代码
type DispatchEntry = {
  event: ReactSyntheticEvent,
  listeners: Array<DispatchListener>,
};

export type DispatchQueue = Array<DispatchEntry>;

-   listeners是事件执行路径 : 从目标节点冒泡至顶层节点所收集的所有同类型事件
-   event是合成事件对象

extractEvents

js 复制代码
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;
  switch (domEventName) {
    case 'keypress':
    case 'mouseout':
    case 'mouseover':
    case 'contextmenu':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case 'drag':
    case 'dragend':
    ...
    default:
      break;
  }

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {
    ...
  } else {
    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';
    // 收集事件
    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});
    }
  }
}

1.合成事件对象

首先根据domEventName选择对应的SyntheticEventCtor ,最后都是调用createSyntheticEvent ,只是传入的EventInterfaceType (浏览器W3C的规范)类型不同;原生的事件对象对应合成事件对象的nativeEvent属性。

js 复制代码
// 构造合成事件对象
const event = new SyntheticEventCtor(
    reactName,
    reactEventType,
    null,
    nativeEvent,
    nativeEventTarget,
  );
  
export const SyntheticKeyboardEvent = createSyntheticEvent(
  KeyboardEventInterface,
);
const KeyboardEventInterface = {
  ...UIEventInterface,
  key: getEventKey,
  code: 0,
  location: 0,
  ctrlKey: 0,
  shiftKey: 0,
  altKey: 0,
  metaKey: 0,
  repeat: 0,
  locale: 0,
  getModifierState: getEventModifierState;
  charCode: function(event) {
   
    if (event.type === 'keypress') {
      return getEventCharCode(event);
    }
    return 0;
  },
  keyCode: function(event) {
    if (event.type === 'keydown' || event.type === 'keyup') {
      return event.keyCode;
    }
    return 0;
  },
  which: function(event) {
    if (event.type === 'keypress') {
      return getEventCharCode(event);
    }
    if (event.type === 'keydown' || event.type === 'keyup') {
      return event.keyCode;
    }
    return 0;
  },
};

function createSyntheticEvent(Interface: EventInterfaceType) {
  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;
  }
  
  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;
    },

    //事件池相关 React17之后以移除了事件池的概念,可能是出于兼容性考虑,保留了persist的引用,
    /**
     * We release all dispatched `SyntheticEvent`s after each event loop, adding
     * them back into the pool. This allows a way to hold onto a reference that
     * won't be added back into the pool.
     */
    persist: function() {
      // Modern event system doesn't use pooling.
    },

    isPersistent: functionThatReturnsTrue,
  });
  return SyntheticBaseEvent;
}

2.收集事件执行路径

这个过程是将组件中真正的事件处理函数收集到数组中,等待下一步的批量执行,函数内部最重要的操作无疑是收集事件到执行路径,为了实现这一操作,需要在fiber树中从触发事件的源fiber节点开始,向上一直找到root(react17后是挂载在root上),形成一条完整的冒泡或者捕获的路径。同时,沿途路过fiber节点时,根据事件名,从props中获取我们真正写在组件中的事件处理函数,push到路径中,等待下一步的批量执行

js 复制代码
// 事件收集
export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
  nativeEvent: AnyNativeEvent,
): Array<DispatchListener> {
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = inCapturePhase ? captureName : reactName;
  let listeners: Array<DispatchListener> = [];

  let instance = targetFiber;
  let lastHostComponent = null;

  // 从事件触发点到root收集事件
  while (instance !== null) {
    const {stateNode, tag} = instance;
    // Handle listeners that are on HostComponents (i.e. <div>)
    if (...) {
         ...
      }
      // Standard React on* listeners, i.e. onClick or onClickCapture
      if (reactEventName !== null) {
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent),
          );
        }
      }
    } else if (
      ...
    ) {
      ...
    }
    if (accumulateTargetOnly) {
      break;
    }
    ...
    instance = instance.return;
   // 从target节点 -> root节点 
  }
  return listeners;
}



export default function getListener(
  inst: Fiber,
  registrationName: string,
): Function | null {
  const stateNode = inst.stateNode;
  if (stateNode === null) {
    // Work in progress (ex: onload events in incremental mode).
    return null;
  }
  //获取fiber节点上所有的props
  const props = getFiberCurrentPropsFromNode(stateNode);
  if (props === null) {
    // Work in progress.
    return null;
  }
  //获取我们写在jsx上的回调函数
  const listener = props[registrationName];
  if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
    return null;
  }

  if (listener && typeof listener !== 'function') {
    throw new Error(
     ...
  }
  return listener;
}

// 返回一个包装对象
function createDispatchListener(
  instance: null | Fiber,
  listener: Function,
  currentTarget: EventTarget,
): DispatchListener {
  return {
    instance,
    listener,
    currentTarget,
  };
}

processDispatchQueue

js 复制代码
// 触发dispatch队列里面的事件
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);
    //  event system doesn't use pooling.
  }
  rethrowCaughtError();
}

模拟捕获冒泡

js 复制代码
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  //收集事件时,无论是冒泡还是捕获,事件都是直接push到路径里的
  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];
    // 如果事件对象阻止了冒泡,则return掉循环过程
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

事件池

事件池(Event Pool)是 React 在 16 及其之前版本中使用的一种事件处理机制,与现在React处理机制不同的一点在于对象重用

对象重用 :事件池的核心思想是在事件处理过程中重用事件对象,从而减少对象的创建和销毁开销。在每次事件触发时,React 会从事件池中取出一个事件对象,并将事件属性填充 为当前触发的事件的相关信息。然后在事件处理完成后,这个事件对象会被重置为初始状态,以便在下一次事件触发时继续使用。

事件池的主要优势在于减少了大量事件对象的创建和销毁开销,然而,它也存在一些问题,比如事件对象被异步修改可能导致不可预测的行为,以及事件对象的属性在 React 内部被修改可能影响到组件之外的代码。

js 复制代码
function App() {
  const handleClick = (e: React.MouseEvent) => {
    console.log('直接打印e', e.target) // <button>React事件池</button>
    // v17以下在异步方法拿不到事件e,必须先调用 e.persist()
    // e.persist()

    // 异步方式获取事件e
    setTimeout(() => {
      console.log('setTimeout打印e', e.target) // null
    })
  }
  return (
    <div className="App">
      <button onClick={handleClick}>React事件池</button>
    </div>
  )
}

也就是说如果我们需要在事件处理函数运行之后获取事件对象的属性,需要调用 e.persist(),它会阻止事件池重置事件对象的操作以及允许我们保留对该事件对象的引用。

事件池的好处是在较旧浏览器中重用了不同事件的事件对象以提高性能,但它对现代浏览器的性能优化微乎其微,反而给开发者带来困惑,因此去除了事件池,因此也没有了事件复用机制。

参考

Sivan - React:合成事件机制

React 深入畅聊 React 事件系统(v17、v18版本)

luxi-record-react-sourceCodeDebug

相关推荐
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端