React合成事件

262 阅读5分钟

动机

  1. 抹平浏览器兼容性
  2. 给事件添加优先级,便于调度
  3. 减少事件绑定(相比vue,vue在事件绑定的地方也有优化(事件缓存,事件绑定的优化),但是仍然需要绑定事件),但是也会产生一个新问题,不管有没有事件处理器,都会需要反向遍历fiber树,并且一般情况下会遍历两遍

实现思路

  1. 根据事件不同获取不同的优先级的事件监听器(dispatchEvent)
  2. 在root元素上绑定对应的事件监听器(dispachEvent),包括捕获阶段和冒泡阶段
  3. 当事件发生的时候,被注册的监听器拦截到
  4. 根据事件的target属性,获取事件发生的真实dom对象(target),根据dom,获取对应的fiber
  5. 根据获取的fiber,根据return属性,向上遍历收集fiber上声明的事件处理函数
  6. 根据事件处理函数列表,并结合是冒泡还是捕获阶段,正向或者反向执行事件函数
  7. 事件函数的执行不是直接调用事件处理函数,而是通过自定义事件做的。

相关代码

function createRoot(container, options){
  // 调用createFiberRoot得到FiberRoot
  var root = createContainer(container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
  markContainerAsRoot(root.current, container);
  var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
  // 监听事件
  listenToAllSupportedEvents(rootContainerElement);
  return new ReactDOMRoot(root);
}
// 监听原声事件
function listenToAllSupportedEvents(rootContainerElement) {
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    allNativeEvents.forEach(function (domEventName) {
      if (domEventName !== 'selectionchange') {
        // nonDelegatedEvents里面存储了不会冒泡的事件
        if (!nonDelegatedEvents.has(domEventName)) {
          // 注册冒泡阶段的监听器,不同的事件类型会获取不同优先级的监听器
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        // 注册捕获阶段的监听器, 不同的事件类型会获取不同优先级的监听器
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    var ownerDocument = rootContainerElement.nodeType === DOCUMENT_NODE ? rootContainerElement : rootContainerElement.ownerDocument;

    if (ownerDocument !== null) {
      // The selectionchange event also needs deduplication
      // but it is attached to the document.
      if (!ownerDocument[listeningMarker]) {
        ownerDocument[listeningMarker] = true;
        listenToNativeEvent('selectionchange', false, ownerDocument);
      }
    }
  }
}
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
  var eventSystemFlags = 0;

  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }

  // 绑定包装事件监听器
  addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
  // 获取事件包装器
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); // If passive option is not supported, then the event will be

  var 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 =  targetContainer;
  var unsubscribeListener; // When legacyFBSupport is enabled, it's for when we


  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);
    }
  }
}
// 根据事件的优先级,获取对应的包装器,实际上就是dispatch方法
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);
}
// 根据事件获取优先级
/*
  DiscreteEventPriority: EventPriority = SyncLane;0b0000000000000000000000000000001
  ContinuousEventPriority: EventPriority = InputContinuousLane; 0b0000000000000000000000000000100
  DefaultEventPriority: EventPriority = DefaultLane; 0b0000000000000000000000000010000
  IdleEventPriority: EventPriority = IdleLane 0b0100000000000000000000000000000;
*/
function getEventPriority(domEventName) {
  switch (domEventName) {
    // Used by SimpleEventPlugin:
    case 'cancel':
    ...
    case 'beforeinput':
      return DiscreteEventPriority;

    case 'drag':
    ...
    case 'pointerleave':
      return ContinuousEventPriority;

    case 'message':
      {
          default:
            return DefaultEventPriority;
        }
      }

    default:
      return DefaultEventPriority;
  }
}
// 添加捕获事件
function addEventCaptureListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, true);
  return listener;
}
// 添加冒泡类型的事件监听器
function addEventBubbleListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, false);
  return listener;
}
function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
  if (blockedOn === null) {
    dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, return_targetInst, targetContainer);
    clearIfContinuousEvent(domEventName, nativeEvent);
    return;
  }
}
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  ...
  batchedUpdates(function () {
    return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
  });
}
function batchedUpdates(fn, a, b) {
  if (isInsideEventHandler) {
    // If we are currently inside another batch, we need to wait until it
    // fully completes before restoring state.
    return fn(a, b);
  }

  isInsideEventHandler = true;

  try {
    return batchedUpdatesImpl(fn, a, b);
  } finally {
    isInsideEventHandler = false;
    finishEventHandler();
  }
}
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var dispatchQueue = [];
  // 根据fiber树遍历分离事件处理器
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // 执行事件处理器
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
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();
}
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;
    }
  }
}
function executeDispatch(event, listener, currentTarget) {
  var type = event.type || 'unknown-event';
  event.currentTarget = currentTarget;
  // 执行事件处理器
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}
function invokeGuardedCallbackDev(name, func, context, a, b, c, d, e, f) {
      // 通过自定义事件执行事件处理函数
      var evt = document.createEvent('Event');
      var didCall = false; 
      var didError = true; // Keeps track of the value of window.event so that we can reset it

      var windowEvent = window.event; // Keeps track of the descriptor of window.event to restore it after event
      // dispatching: https://github.com/facebook/react/issues/13688

      var windowEventDescriptor = Object.getOwnPropertyDescriptor(window, 'event');

      function restoreAfterDispatch() {
        fakeNode.removeEventListener(evtType, callCallback, false); // We check for window.hasOwnProperty('event') to prevent the
        if (typeof window.event !== 'undefined' && window.hasOwnProperty('event')) {
          window.event = windowEvent;
        }
      } // Create an event handler for our fake event. We will synchronously
      // dispatch our fake event using `dispatchEvent`. Inside the handler, we
      // call the user-provided callback.


      var funcArgs = Array.prototype.slice.call(arguments, 3);

      function callCallback() {
        didCall = true;
        restoreAfterDispatch();
        func.apply(context, funcArgs);
        didError = false;
      } // Create a global error event handler. We use this to capture the value

      var error; // Use this to track whether the error event is ever called.

      var didSetError = false;
      var isCrossOriginError = false;

      function handleWindowError(event) {
        error = event.error;
        didSetError = true;

        if (error === null && event.colno === 0 && event.lineno === 0) {
          isCrossOriginError = true;
        }

        if (event.defaultPrevented) {
          // Some other error handler has prevented default.
          // Browsers silence the error report if this happens.
          // We'll remember this to later decide whether to log it or not.
          if (error != null && typeof error === 'object') {
            try {
              error._suppressLogging = true;
            } catch (inner) {// Ignore.
            }
          }
        }
      } // Create a fake event type.


      var evtType = "react-" + (name ? name : 'invokeguardedcallback'); // Attach our event handlers

      window.addEventListener('error', handleWindowError);
      // 事件处理函数绑定
      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);

      if (windowEventDescriptor) {
        Object.defineProperty(window, 'event', windowEventDescriptor);
      }

      if (didCall && didError) {
        if (!didSetError) {
          // The callback errored, but the error event never fired.
          // eslint-disable-next-line react-internal/prod-error-codes
          error = new Error('An error was thrown inside one of your components, but React ' + "doesn't know what it was. This is likely due to browser " + 'flakiness. React does its best to preserve the "Pause on ' + 'exceptions" behavior of the DevTools, which requires some ' + "DEV-mode only tricks. It's possible that these don't work in " + 'your browser. Try triggering the error in production mode, ' + 'or switching to a modern browser. If you suspect that this is ' + 'actually an issue with React, please file an issue.');
        } else if (isCrossOriginError) {
          // eslint-disable-next-line react-internal/prod-error-codes
          error = new Error("A cross-origin error was thrown. React doesn't have access to " + 'the actual error object in development. ' + 'See https://reactjs.org/link/crossorigin-error for more information.');
        }

        this.onError(error);
      } // Remove our event listeners


      window.removeEventListener('error', handleWindowError);

      if (!didCall) {
        restoreAfterDispatch();
        return invokeGuardedCallbackProd.apply(this, arguments);
      }
    };
  }
}

Debug

TODO

  1. 为什么不直接执行事件函数而是通过触发自定义事件的方式执行