react 事件系统源码学习

  • React利用事件委托机制在根DOM容器上统一监听DOM事件,再根据触发的target将事件分发到具体的组件实例。另外event参数是一个合成事件对象(SyntheticEvent), 而不是原始的DOM事件对象.
  • 不是直接触发绑定的事件回调,而是先触发绑定到根DOM容器上的对应类型事件,根据event.target找到触发事件的节点,然后将合成事件的event传入事件回调中触发
  • 只要有正确的event,事件和由谁来触发没有任何关系

React自定义的事件系统特点:

  • 抹平浏览器之间的兼容性差异。
  • 事件自定义,表单onChange事件,它为表单元素定义了统一的值变动事件。也可以通过React的事件插件机制来合成自定义事件
  • 抽象跨平台事件机制
  • 利用事件委托机制,简化了DOM事件处理逻辑,减少了内存开销. 但这也意味着,React需要自己模拟一套事件冒泡的机制。
  • 区分事件优先级

从v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中.

事件注册:

<div onClick={this.handle}></div>
  • 实际上onClick方法会传入fiber的props中,并不像原生事件监听。

registerDirectEvent

  • 把react事件名称跟对应的dependencies关系存到registrationNameDependencies中:维护一个数据结构 {‘onclick’:[‘click’],… }
  • 把所有的依赖存入allNativeEvents中
    • 非委派事件,即不支持冒泡的事件,不会存入,主要是媒体事件video、scroll等会绑在自己对应的el节点上
function registerDirectEvent(registrationName, dependencies) {
  ...
  registrationNameDependencies[registrationName] = dependencies;
  {
    var lowerCasedName = registrationName.toLowerCase();
    possibleRegistrationNames[lowerCasedName] = registrationName;

    if (registrationName === 'onDoubleClick') {
      possibleRegistrationNames.ondblclick = registrationName;
    }
  }
  for (var i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

在这里插入图片描述
创建fiberRoot对象时, 调用createRootImpl:

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // ... 省略无关代码
  if (enableEagerRootListeners) {
    const rootContainerElement =
      container.nodeType === COMMENT_NODE ? container.parentNode : container;
    listenToAllSupportedEvents(rootContainerElement);
  }
  // ... 省略无关代码
}

listenToAllSupportedEvents

  • 遍历事件名称,调用listenToNativeEvent监听冒泡或捕获阶段的事件
var mediaEventTypes = ['abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting']; 
var nonDelegatedEvents = new Set(['cancel', 'close', 'invalid', 'load', 'scroll', 'toggle'].concat(mediaEventTypes));

// ... 
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (enableEagerRootListeners) {
    // 1. 节流优化, 保证全局注册只被调用一次
    if ((rootContainerElement: any)[listeningMarker]) {
      return;
    }
    (rootContainerElement: any)[listeningMarker] = true;
    // 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
    allNativeEvents.forEach(domEventName => {
     //不支持委托的事件,即不支持冒泡的事件不会进入
      if (!nonDelegatedEvents.has(domEventName)) {
        listenToNativeEvent(
          domEventName,
          false, // 冒泡阶段监听
          ((rootContainerElement: any): Element),
          null,
        );
      }
      listenToNativeEvent(
        domEventName,
        true, // 捕获阶段监听
        ((rootContainerElement: any): Element),
        null,
      );
    });
  }
}


listenToNativeEvent

  • 通过addTrappedEventListener监听事件
// ... 
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  rootContainerElement: EventTarget,
  targetElement: Element | null,
  eventSystemFlags?: EventSystemFlags = 0,
): void {
  let target = rootContainerElement;
  
  //获取存储事件的集合
  const listenerSet = getEventListenerSet(target);
  //获取遍历的事件的key
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener,
  );
  // 利用set数据结构, 保证相同的事件类型只会被注册一次.
  if (!listenerSet.has(listenerSetKey)) {
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    // 注册事件监听
    addTrappedEventListener(
      target,
      domEventName,
      eventSystemFlags,
      isCapturePhaseListener,
    );
    listenerSet.add(listenerSetKey);
  }
}

addTrappedEventListener

  • 根据事件名称,构造不同类型事件的listener回调
  • addEventCaptureListener、addEventBubbleListener往根容器上注册事件
  • 后续节点上触发的监听会是对应的listener
// ... 
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  // 1. 构造不同类型事件的listener回调
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
  let unsubscribeListener;
  // 2. 注册事件监听
  if (isCapturePhaseListener) {
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener,
    );
  } else {
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener,
    );
  }
}

// 注册原生事件 冒泡
export function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, false);
  return listener;
}

// 注册原生事件 捕获
export function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, true);
  return listener;
}

createEventListenerWrapperWithPriority

  • 根据不同类型事件的优先级,构造listener事件回调
    • DiscreteEvent 离散事件. 例如blur、focus、 click、 submit、 touchStart. 这些事件都是离散触发的

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

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

    • 对应scheduler中的优先级

      • Immediate - 这个优先级的任务会同步执行, 或者说要马上执行且不能中断,对应ContinuousEvent
      • UserBlocking(250ms timeout) 这些任务一般是用户交互的结果, 需要即时得到反馈 .对应UserBlockingEvent(先执行)、DiscreteEvent(后执行)
      • Normal (5s timeout) 应对哪些不需要立即感受到的任务,例如网络请求
      • Low (10s timeout) 这些任务可以放后,但是最终应该得到执行. 例如分析通知
      • Idle (no timeout) 一些没有必要做的任务 (e.g. 比如隐藏的内容).
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 1. 根据优先级设置 listenerWrapper
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  // 2. 返回 listenerWrapper
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

dispatchEvent

// ...省略无关代码
export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  if (!_enabled) {
    return;
  }
  const blockedOn = attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent,
  );
}

attemptToDispatchEvent

  • 把原生事件和fiber树关联起来.
export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
  // ...

  // 1. 定位原生DOM节点
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 2. 获取与DOM节点对应的fiber节点
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  // 3. 通过插件系统, 派发事件
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  return null;
}

在这里插入图片描述

  • SimpleEventPlugin - 简单事件, 处理一些比较通用的事件类型,例如click、input、keyDown、mouseOver、mouseOut、pointerOver、pointerOut
  • EnterLeaveEventPlugin - mouseEnter/mouseLeave和pointerEnter/pointerLeave这两类事件比较特殊, 和*over/*out事件相比, 它们不支持事件冒泡, enter会给所有进入的元素发送事件, 行为有点类似于:hover; 而over在进入元素后,还会冒泡通知其上级. react通过over、out事件来模拟的enter、leave事件
  • ChangeEventPlugin - change事件是React的一个自定义事件,旨在规范化表单元素的变动事件。它支持这些表单元素: input, textarea, select
  • SelectEventPlugin - 和change事件一样,React为表单元素规范化了select(选择范围变动)事件,适用于input、textarea、contentEditable元素
  • BeforeInputEventPlugin - beforeinput事件以及composition事件处理

在之前的版本中,为了避免频繁创建和释放事件对象导致性能损耗(对象创建和垃圾回收),React使用一个事件池来负责管理事件对象,使用完的事件对象会放回池中,以备后续的复用使用一个对象,在事件处理器同步执行完后,SyntheticEvent对象就会马上被回收,所有属性都会无效, 也就是说SyntheticEvent不能用于异步引用,它在同步执行完事件处理器后就会被释放。在异步操作中访问SyntheticEvent事件对象,通过event.persist()方法,告诉React不要回收到对象池

收集对应 fiber 上的 注册的事件回调

SimpleEventPlugin.extractEvents

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;
  }
  //合成事件event的构造方法
  let SyntheticEventCtor = SyntheticEvent;
  
  let reactEventType: string = domEventName;

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  //如果是scroll事件或捕获事件,不向上收集
  const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
  
  // 1. 收集所有监听该事件的函数.
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
  );
  
  if (listeners.length > 0) {
    // 2. 构造合成事件的event, 添加到派发队列
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget,
    );
    //存储该事件所有的合成event和事件回调
    dispatchQueue.push({ event, listeners });
  }
}

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> = [];
  
  //触发事件的fiber节点
  let instance = targetFiber;
  let lastHostComponent = null;

  // 从targetFiber开始, 向上遍历, 直到 root 为止
  while (instance !== null) {
    const { stateNode, tag } = instance;
    // 当节点类型是HostComponent时(如: div, span, button等类型)
    if (tag === HostComponent && stateNode !== null) {
      //获取fiber节点对应的实例
      lastHostComponent = stateNode;
      if (reactEventName !== null) {
        // 获取标准的监听函数 (如onClick , onClickCapture等)
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent),
          );
        }
      }
    }
    // 如果只收集目标节点, 则不用向上遍历, 直接退出(捕获事件和scroll)
    if (accumulateTargetOnly) {
      break;
    }
    instance = instance.return;
  }
  return listeners;
}

合成事件的event
SyntheticEvent

  • 原生event事件的包装器,抹平不同浏览器 api 的差异
type EventInterfaceType = {
  [propName: string]: 0 | ((event: {[propName: string]: mixed}) => mixed),
};

function functionThatReturnsTrue() {
  return true;
}

function functionThatReturnsFalse() {
  return false;
}

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;
    
    //遍历设置event上的属性
    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;
      }
      //兼容不同平台的preventDefault
      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;
      }
	  //兼容不同平台的stopPropagation
      if (event.stopPropagation) {
        event.stopPropagation();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.cancelBubble !== 'unknown') {
        // The ChangeEventPlugin registers a "propertychange" event for
        // IE. This event does not support bubbling or cancelling, and
        // any references to cancelBubble throw "Member not found".  A
        // typeof check of "unknown" circumvents this issue (and is also
        // IE specific).
        event.cancelBubble = true;
      }

      this.isPropagationStopped = functionThatReturnsTrue;
    },

    /**
     * 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.
    },

    /**
     * Checks if this event should be released back into the pool.
     *
     * @return {boolean} True if this should not be released, false otherwise.
     */
    isPersistent: functionThatReturnsTrue,
  });
  return SyntheticBaseEvent;
}

执行派发事件

  • 遍历dispatchListeners数组, 执行executeDispatch派发事件, 在fiber节点上绑定的回调函数被执行.

processDispatchQueue

export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  //遍历之前存储的dispatchQueue
  for (let i = 0; i < dispatchQueue.length; i++) {
    const { event, listeners } = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
  // ...
}

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  
  if (inCapturePhase) {
    // 1. capture事件: 倒序遍历listeners,因为是向上收集的事件,所以捕获事件需要倒叙触发
    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 {
    // 2. bubble事件: 顺序遍历listeners,因为是向上收集的事件,所以冒泡事件需要顺序触发
    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;
    }
  }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值