大年初四,快来学 React 合成事件

186 阅读6分钟

背景

相传在很久很久以前,浏览器有好多不同的类别,比如谷歌内核,IE 内核,火狐内核...每个内核开发的浏览器,绑定事件的方式和对事件的一些处理都有一些微妙的差别,React 为了抹平差异,其内部对基础事件进行了一层封装,使得开发者更加方便快速的进行开发。

大纲

在 React 中对事件的处理分为事件注册和事件派发。

事件注册:

事件派发

listenToAllSupportedEvents(事件注册)

function listenToAllSupportedEvents(rootContainerElement) {
  // ...
   allNativeEvents.forEach(function (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);
          }
        });
  // ...
}

listenToAllSupportedEvents函数接收一个参数,这个参数是当前绑定 React 的根节点,比如

。同时allNativeEvents是一个事件名称的集合,通过遍历allNativeEvents调用两次listenToNativeEvent,分别对应着事件捕获和事件冒泡。

其核心方法是listenToNativeEvent,下面来看一下这个函数

listenToNativeEvent

function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
  // ...
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener
  );
  // ...
}

这个函数也很简单,主要是调用了addTrappedEventListener来对事件进行了处理,我们来一看究竟。

addTrappedEventListener

function addTrappedEventListener(
  targetContainer,
  domEventName,
  eventSystemFlags,
  isCapturePhaseListener,
  isDeferredListenerForLegacyFBSupport
) {
   var listener = createEventListenerWrapperWithPriority(
        targetContainer,
        domEventName,
        eventSystemFlags
      );
      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
          );
        }
      }
  
}
  1. 通过createEventListenerWrapperWithPriority创建了一个监听器
  2. 通过 addEventXXX进行事件的注册。

分别来看一下这两块

createEventListenerWrapperWithPriority

function createEventListenerWrapperWithPriority(
  targetContainer,
  domEventName,
  eventSystemFlags
) {
  // ...
    var eventPriority = getEventPriority(domEventName);
      var listenerWrapper;

      switch (eventPriority) {
        case DiscreteEventPriority:
          listenerWrapper = dispatchDiscreteEvent;
          break;

        case ContinuousEventPriority:
          listenerWrapper = dispatchContinuousEvent;
          break;

        case DefaultEventPriority:
        default:
          listenerWrapper = dispatchEvent;
          break;
      }

      return listenerWrapper.bind(
        null,
        domEventName,
        eventSystemFlags,
        targetContainer
      );
}
  1. 获取事件优先级
  2. 通过不同的优先级返回不同的触发事件的函数:dispatchXXX
  3. 通过bind 绑定预制参数。

获取事件优先级比较简单

    function getEventPriority(domEventName) {
      switch (domEventName) {
        // Used by SimpleEventPlugin:
        case "cancel":
        case "click":
        case "close":
        case "contextmenu":
        case "copy":
        case "cut":
        case "auxclick":
        case "dblclick":
        case "dragend":
        case "dragstart":
        case "drop":
        case "focusin":
        case "focusout":
        case "input":
        case "invalid":
        case "keydown":
        case "keypress":
        case "keyup":
        case "mousedown":
        case "mouseup":
        case "paste":
        case "pause":
        case "play":
        case "pointercancel":
        case "pointerdown":
        case "pointerup":
        case "ratechange":
        case "reset":
        case "resize":
        case "seeked":
        case "submit":
        case "touchcancel":
        case "touchend":
        case "touchstart":
        case "volumechange": // Used by polyfills:
        // eslint-disable-next-line no-fallthrough

        case "change":
        case "selectionchange":
        case "textInput":
        case "compositionstart":
        case "compositionend":
        case "compositionupdate": // Only enableCreateEventHandleAPI:
        // eslint-disable-next-line no-fallthrough

        case "beforeblur":
        case "afterblur": // Not used by React but could be by user code:
        // eslint-disable-next-line no-fallthrough

        case "beforeinput":
        case "blur":
        case "fullscreenchange":
        case "focus":
        case "hashchange":
        case "popstate":
        case "select":
        case "selectstart":
          return DiscreteEventPriority;

        case "drag":
        case "dragenter":
        case "dragexit":
        case "dragleave":
        case "dragover":
        case "mousemove":
        case "mouseout":
        case "mouseover":
        case "pointermove":
        case "pointerout":
        case "pointerover":
        case "scroll":
        case "toggle":
        case "touchmove":
        case "wheel": // Not used by React but could be by user code:
        // eslint-disable-next-line no-fallthrough

        case "mouseenter":
        case "mouseleave":
        case "pointerenter":
        case "pointerleave":
          return ContinuousEventPriority;

        case "message": {
          // We might be in the Scheduler callback.
          // Eventually this mechanism will be replaced by a check
          // of the current priority on the native scheduler.
          var schedulerPriority = getCurrentPriorityLevel();

          switch (schedulerPriority) {
            case ImmediatePriority:
              return DiscreteEventPriority;

            case UserBlockingPriority:
              return ContinuousEventPriority;

            case NormalPriority:
            case LowPriority:
              // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
              return DefaultEventPriority;

            case IdlePriority:
              return IdleEventPriority;

            default:
              return DefaultEventPriority;
          }
        }

        default:
          return DefaultEventPriority;
      }
    }

可以看到就是根据事件名返回不同的优先级。

addEventXXXListener

function addEventCaptureListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, true);
  return listener;
}

这里可以看到就是将事件委托到了根dom上。

总结

  1. allNativeEvents维护了所有的事件名,通过遍历这个数组,将所有的事件委托到根dom 上。
  2. 总共有两次listenToNativeEvent的调用:事件捕获、事件委托。
  3. 创建一个listener监听器,然后注册到根 dom。

dispatchDiscreteEvent(事件派发)

这是一个组件,当点击 onClick 时,由于所有的事件都委托到了根 dom 节点,通过根 dom 节点进行派发。派发的入口函数是dispatchXXX,就是事件注册时 switch 返回的那个函数。我们直接看dispatchEvent

function dispatchEvent(){
  // ...
  var blockedOn = attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent
  );
  //...
}

核心方法是attemptToDispatchEvent,直接看这个函数

attemptToDispatchEvent

function attemptToDispatchEvent(
  domEventName,
  eventSystemFlags,
  targetContainer,
  nativeEvent
) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var targetInst = getClosestInstanceFromNode(nativeEventTarget);
  dispatchEventForPluginEventSystem(
        domEventName,
        eventSystemFlags,
        nativeEvent,
        targetInst,
        targetContainer
      );
}
  1. 根据nativeEvent获取真实触发事件的 dom 元素。
  2. 获取 dom 元素对应的 Fiber 节点。
  3. dispatchEventForPluginEventSystem

dispatchEventForPluginEventSystem

function dispatchEventForPluginEventSystem(){
  batchedUpdates(function () {
    return dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst
    );
  });
}

主要就是通过batchedUpdates来执行匿名函数,重要的是dispatchEventsForPlugins

dispatchEventsForPlugins

function dispatchEventsForPlugins(
  domEventName,
  eventSystemFlags,
  nativeEvent,
  targetInst,
  targetContainer
) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var dispatchQueue = [];
  extractEvents$5(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags
  );
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

这个方法就比较核心了,主要干了三件事情,我们一个一个看:

  1. 通过nativeEvent获取对应的真实触发事件的 dom 元素。
  2. extractEvents$5:主要就是根据 fiberNode 以及事件名获取开发者写好的事件回调函数。
  3. processDispatchQueue

extractEvents$5

function extractEvents$5(){
  extractEvents$4(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags
  );
}

extractEvents$4

function extractEvents$4(){
  // ...
  var _listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly
  );
  // ...

  if (_listeners.length > 0) {
    // Intentionally create event lazily.
    var _event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget
    );

    dispatchQueue.push({
      event: _event,
      listeners: _listeners,
    });
  }
}
  1. 通过accumulateSinglePhaseListeners获取开发者注册的事件回调
  2. 创建一个 event 对象,然后将这次的事件任务入 dispatchQueue 的任务队列。
accumulateSinglePhaseListeners
function accumulateSinglePhaseListeners(
  targetFiber,
  reactName,
  nativeEventType,
  inCapturePhase,
  accumulateTargetOnly,
  nativeEvent
) {
  //...
  var listener = getListener(instance, reactEventName);
  if (listener != null) {
    listeners.push(
      createDispatchListener(instance, listener, lastHostComponent)
    );
  }
  // ...
}
  1. 通过getListener获得 FiberNode 上开发者注册的函数。
function getListener(inst, registrationName) {
      var stateNode = inst.stateNode;

      if (stateNode === null) {
        // Work in progress (ex: onload events in incremental mode).
        return null;
      }

      var props = getFiberCurrentPropsFromNode(stateNode);

      if (props === null) {
        // Work in progress.
        return null;
      }

      var listener = props[registrationName];

      if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
        return null;
      }

      if (listener && typeof listener !== "function") {
        throw new Error(
          "Expected `" +
            registrationName +
            "` listener to be a function, instead got a value of `" +
            typeof listener +
            "` type."
        );
      }

      return listener;
    }

2. 将函数入队列。

processDispatchQueue

function processDispatchQueue(dispatchQueue, eventSystemFlags) {
  var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

  for (var i = 0; i < dispatchQueue.length; i++) {
    var _dispatchQueue$i = dispatchQueue[i],
      event = _dispatchQueue$i.event,
      listeners = _dispatchQueue$i.listeners;
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase); //  event system doesn't use pooling.
  } // This would be a good time to rethrow if any of the event handlers threw.

  rethrowCaughtError();
}
  1. 遍历dispatchQueue,拿出listeners。
  2. processDispatchQueueItemsInOrder
processDispatchQueueItemsInOrder
function processDispatchQueueItemsInOrder(
  event,
  dispatchListeners,
  inCapturePhase
) {
  var previousInstance;

  if (inCapturePhase) {
    for (var i = dispatchListeners.length - 1; i >= 0; i--) {
      var _dispatchListeners$i = dispatchListeners[i],
        instance = _dispatchListeners$i.instance,
        currentTarget = _dispatchListeners$i.currentTarget,
        listener = _dispatchListeners$i.listener;

      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }

      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (var _i = 0; _i < dispatchListeners.length; _i++) {
      var _dispatchListeners$_i = dispatchListeners[_i],
        _instance = _dispatchListeners$_i.instance,
        _currentTarget = _dispatchListeners$_i.currentTarget,
        _listener = _dispatchListeners$_i.listener;

      if (_instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }

      executeDispatch(event, _listener, _currentTarget);
      previousInstance = _instance;
    }
  }
}
  1. 通过inCapturePhase判断是捕获阶段还是冒泡阶段。
  2. 遍历dispatchListeners,executeDispatch执行
executeDispatch
function executeDispatch(event, listener, currentTarget) {
      var type = event.type || "unknown-event";
      event.currentTarget = currentTarget;
      invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
      event.currentTarget = null;
    }
  1. invokeGuardedCallbackAndCatchFirstError
invokeGuardedCallbackAndCatchFirstError

这个函数内部调用了真正的回调进行执行。

fakeNode.addEventListener(evtType, callCallback, false); // Synchronously dispatch our fake event. If the user-provided function
// errors, it will trigger our global error handler.

evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);

可以看到就是监听了一个事件监听,然后直接dispatch触发。

常见用法原理

阻止事件冒泡

在进行事件处理时,会涉及阻止事件冒泡的操作,一般都是用e.stopPropagation()进行阻止。但是从上面的分析中我们知道,React 是合成事件,通过收集 JSX 上的事件注册函数,然后遍历来调用。那用的应该不是本身自带的冒泡方式。下面来分析一下如何办到的。

看一下这段代码

<div
  onClick={() => {
    console.log("向上冒泡");
  }}
  >
  包裹住
  <button
    onClick={(e) => {
      e.stopPropagation()
      debugger;
    }}
    >
    测试事件
  </button>
</div>

可以看到这里有两个事件回调,正常会先触发 button 的事件回调,然后向上冒泡到 div,触发 div 的事件回调。

但是我这里通过stopPropagation阻止了事件冒泡,所以不会冒泡到 div。我们一看究竟。

for (var _i = 0; _i < dispatchListeners.length; _i++) {
  var _dispatchListeners$_i = dispatchListeners[_i],
    _instance = _dispatchListeners$_i.instance,
    _currentTarget = _dispatchListeners$_i.currentTarget,
    _listener = _dispatchListeners$_i.listener;

  if (_instance !== previousInstance && event.isPropagationStopped()) {
    return;
  }

  executeDispatch(event, _listener, _currentTarget);
  previousInstance = _instance;
}

这里通过遍历收集的dispatchListeners,然后循环调用。这里分析一下stopPropagation干了什么

function () {
  var 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;
}
  1. 首先进行兼容,IE 内核和谷歌内核的取消事件冒泡方式是不同的,所以这里进行了判断,分别进行兼容。
  2. 将isPropagationStopped设置为 true。
  3. 然后在循环调用的时候,就走到了 if 里,直接 return 了,这就是阻止冒泡的原理。

总结

事件注册

  1. 对事件捕获和事件冒泡分别进行处理。
  2. 将所有事件注册到根 dom 元素上。
  3. 对listeners事件函数进行包装,注册到根 dom 上。

事件派发

  1. 触发到根 root。
  2. 收集注册到事件函数到数组中。
  3. 循环遍历数组进行执行。同时还对冒泡相关进行了包装,抹平了浏览器的差异。