React之事件机制源码解析

定义

事件委托机制:并不是将Click事件绑定在DOM上,而是采用事件冒泡的形式冒泡到document上,然后将React事件内容封装并交真正的函数处理

合成事件SyntheticEvent:冒泡到document上的事件也不是原生的浏览器事件,而是由react实现的合成事件SyntheticEvent

⚠️
如果不想要事件冒泡的话应该调用event.preventDefault()方法,而不是event.stopProppagation(),event.stopProppagation()
只能阻止虚拟DOM的事件冒泡,但它本身是由document触发的,document 就是冒泡的顶点

事件触发过程

  • 监听原生事件,将原生DOM元素与fiber树进行关联,对齐原生DOM结点与fiber节点
  • 遍历fiber树,通过事件插件系统收集所有监听该事件的listener
  • 通过构造函数SyntheticBaseEvent 构造该事件的合成事件,抹平不同浏览器之间的事件兼容性问题
  • 最后分别在捕获阶段和冒泡阶段通过不同的方式遍历dispatchListeners,执行executeDispatch事件,在fiber节点上绑定的listener被执行

实现SyntheticBaseEvent

  • 事件委派:React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数
  • 自动绑定:React组件中,每个方法的上下文都会指向该组件的实例,自动绑定this为当前组件

用处

  • 减少内存消耗;
  • 能在组件挂载销毁时统一订阅和移除事件,方便react统一管理和事务机制
  • 屏蔽底层不同浏览器之间的事件系统差异,兼容所有浏览器,更好跨平台

源码解析

SimpleEventPlugin.extractEvents:主要调用事件插件来收集listener,提供React事件系统的基本功能
  • 首先将SyntheticEvent赋值给SyntheticEventCtorSyntheticEvent是React内部的对象(构造函数),是原生事件的跨浏览器包装器,拥有和浏览器原生事件相同的接口(stopPropagationpreventDefault),抹平了不同浏览器的事件兼容性问题。
  • 根据原生事件名称,初始化相应的合成事件构造函数 SyntheticEventCtor
  • 分别在捕获阶段和冒泡阶段收集节点上所有监听该事件的 listener
  • 通过合成事件构造函数 SyntheticEventCtor 构造合成事件,将合成事件添加到派发事件队列中。
// packages/react-dom/src/events/plugins/SimpleEventPlugin.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;
  // react 事件系统中的事件类型
  let reactEventType: string = domEventName;
  switch (domEventName) {
    case 'keypress':
      // Firefox creates a keypress event for function keys too. This removes
      // the unwanted keypress events. Enter is however both printable and
      // non-printable. One would expect Tab to be as well (but it isn't).
      if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
        return;
      }
    /* falls through */
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent; // 键盘合成事件
      break;
    case 'focusin':
      reactEventType = 'focus';
      SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
      break;
    case 'focusout':
      reactEventType = 'blur';
      SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
      break;
    case 'beforeblur':
    case 'afterblur':
      SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
      break;
    case 'click':
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return;
      }
    /* falls through */
    case 'auxclick':
    case 'dblclick':
    case 'mousedown':
    case 'mousemove':
    case 'mouseup':
    // TODO: Disabled elements should not respond to mouse events
    /* falls through */
    case 'mouseout':
    case 'mouseover':
    case 'contextmenu':
      SyntheticEventCtor = SyntheticMouseEvent;   // 鼠标合成事件
      break;
    case 'drag':
    case 'dragend':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'dragstart':
    case 'drop':
      SyntheticEventCtor = SyntheticDragEvent;   // 拖拽合成事件
      break;
    case 'touchcancel':
    case 'touchend':
    case 'touchmove':
    case 'touchstart':
      SyntheticEventCtor = SyntheticTouchEvent;   // 移动端触摸合成事件
      break;
    case ANIMATION_END:
    case ANIMATION_ITERATION:
    case ANIMATION_START:
      SyntheticEventCtor = SyntheticAnimationEvent;   // 动画合成事件
      break;
    case TRANSITION_END:
      SyntheticEventCtor = SyntheticTransitionEvent;   // 动画合成事件
      break;
    case 'scroll':
      SyntheticEventCtor = SyntheticUIEvent;     // 滚动合成事件
      break;
    case 'wheel':
      SyntheticEventCtor = SyntheticWheelEvent;    // 滚轮合成事件
      break;
    case 'copy':
    case 'cut':
    case 'paste':
      SyntheticEventCtor = SyntheticClipboardEvent;  // 复制/粘贴/剪切 合成事件
      break;
    case 'gotpointercapture':
    case 'lostpointercapture':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'pointerup':
      SyntheticEventCtor = SyntheticPointerEvent;  
      break;
    default:
      // Unknown event. This is used by createEventHandle.
      break;
  }

  // 捕获阶段
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {

    // 捕获阶段

    // 收集所有监听该事件的 listener
    const listeners = accumulateEventHandleNonManagedNodeListeners(
      // TODO: this cast may not make sense for events like
      // "focus" where React listens to e.g. "focusin".
      ((reactEventType: any): DOMEventName),
      targetContainer,
      inCapturePhase,
    );
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      // 构造合成事件, 添加到派发队列
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      dispatchQueue.push({event, listeners});
    }
  } else {
    // Some events don't bubble in the browser.
    // In the past, React has always bubbled them, but this can be surprising.
    // We're going to try aligning closer to the browser behavior by not bubbling
    // them in React either. We'll start by not bubbling onScroll, and then expand.
    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';

      // 冒泡阶段

      // 收集节点上所有监听该事件的 listener
    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});
    }
  }
}
SyntheticEventCtor – 构造合成事件

根据事件的不同有不同的实现方式,通过调用createSyntheticEvent 函数创建新函数

//packages/react-dom/src/events/SyntheticEvent.js
switch (domEventName) {
  case 'keypress':
    // Firefox creates a keypress event for function keys too. This removes
    // the unwanted keypress events. Enter is however both printable and
    // non-printable. One would expect Tab to be as well (but it isn't).
    if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
      return;
    }
  /* falls through */
  case 'keydown':
  case 'keyup':
    SyntheticEventCtor = SyntheticKeyboardEvent; // 键盘合成事件
    break;
  case 'focusin':
    reactEventType = 'focus';
    SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
    break;
  case 'focusout':
    reactEventType = 'blur';
    SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
    break;
  case 'beforeblur':
  case 'afterblur':
    SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
    break;
  case 'click':
    // Firefox creates a click event on right mouse clicks. This removes the
    // unwanted click events.
    if (nativeEvent.button === 2) {
      return;
    }
  /* falls through */
  case 'auxclick':
  case 'dblclick':
  case 'mousedown':
  case 'mousemove':
  case 'mouseup':
  // TODO: Disabled elements should not respond to mouse events
  /* falls through */
  case 'mouseout':
  case 'mouseover':
  case 'contextmenu':
    SyntheticEventCtor = SyntheticMouseEvent;   // 鼠标合成事件
    break;
  case 'drag':
  case 'dragend':
  case 'dragenter':
  case 'dragexit':
  case 'dragleave':
  case 'dragover':
  case 'dragstart':
  case 'drop':
    SyntheticEventCtor = SyntheticDragEvent;   // 拖拽合成事件
    break;
  case 'touchcancel':
  case 'touchend':
  case 'touchmove':
  case 'touchstart':
    SyntheticEventCtor = SyntheticTouchEvent;   // 移动端触摸合成事件
    break;
  case ANIMATION_END:
  case ANIMATION_ITERATION:
  case ANIMATION_START:
    SyntheticEventCtor = SyntheticAnimationEvent;   // 动画合成事件
    break;
  case TRANSITION_END:
    SyntheticEventCtor = SyntheticTransitionEvent;   // 动画合成事件
    break;
  case 'scroll':
    SyntheticEventCtor = SyntheticUIEvent;     // 滚动合成事件
    break;
  case 'wheel':
    SyntheticEventCtor = SyntheticWheelEvent;    // 滚轮合成事件
    break;
  case 'copy':
  case 'cut':
  case 'paste':
    SyntheticEventCtor = SyntheticClipboardEvent;  // 复制/粘贴/剪切 合成事件
    break;
  case 'gotpointercapture':
  case 'lostpointercapture':
  case 'pointercancel':
  case 'pointerdown':
  case 'pointermove':
  case 'pointerout':
  case 'pointerover':
  case 'pointerup':
    SyntheticEventCtor = SyntheticPointerEvent;  
    break;
  default:
    // Unknown event. This is used by createEventHandle.
    break;
}
createSyntheticEvent函数
  • 是一个工厂函数,不同合成事件的处理函数都是通过它构造的。
  • createSyntheticEvent函数中,定义了一个合成事件的构造函数SyntheticBaseEvent ,除了_reactName_targetInsttypenativeEventtargetcurrentTarget 这些基本的实例属性,在构造相应的合成事件处理函数时把原生事件的属性也添加到了实例对象上。
  • 构造完合成事件之后,连同收集的listener,一起放入到 dispatchQueue中,等待派发。接下来我们来看看React的事件派发。
// packages/react-dom/src/events/SyntheticEvent.js

// This is intentionally a factory so that we have different returned constructors.
// If we had a single constructor, it would be megamorphic and engines would deopt.
function createSyntheticEvent(Interface: EventInterfaceType) {
  /**
   * Synthetic events are dispatched by event plugins, typically in response to a
   * top-level event delegation handler.
   *
   * These systems should generally use pooling to reduce the frequency of garbage
   * collection. The system should check `isPersistent` to determine whether the
   * event should be released into the pool after being dispatched. Users that
   * need a persisted event should invoke `persist`.
   *
   * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
   * normalizing browser quirks. Subclasses do not necessarily have to implement a
   * DOM interface; custom application-specific events can also subclass this.
   */

  // 合成事件构造函数
  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;
  }

  Object.assign(SyntheticBaseEvent.prototype, {
    // 在合成事件构造函数的原型上添加 原生事件的 preventDefault 方法,用于阻止事件的默认行为
    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 方法,用于阻止事件冒泡到父元素
    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') {
        // 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;
}

总结

  1. 在React中不把Click事件绑定在DOM上,而是采用事件冒泡的形式绑定到document上,再将这些事件内容封装起来交给真正的函数处理
  2. 阻止冒泡只能使用event.preventDefault()方法
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值