React事件原理

108 阅读3分钟

为什么使用合成事件 ?

  • 合成事件 它符合W3C规范,在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口.
  • 大部分处理逻辑都在底层处理了,这对后期的 ssr 和跨端支持度很高.

原理

🔥 DEMO

import {  createPortal } from 'react-dom'

function Child(){
  return <button onClick={()=>console.log('child')}>Child</button>
}

function App() {

    return <div onClick={()=>console.log('parent')}>
     <Child />
     {createPortal( <button onClick={()=>console.log('portal')}>Portal</button>,document.body)}
  </div>
}

事件绑定

  • 🔥 事件绑定都依赖于addTrappedEventListener完成
function addTrappedEventListener(targetContainer, domEventName,isCapturePhaseListener){
  // 创建 react 事件处理函数
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName);
  //
   if(isCapturePhaseListener){
      addEventCaptureListener(targetContainer, domEventName, listener)
   } else {
      addEventBubbleListener(targetContainer, domEventName, listener)
   }
}

function addEventBubbleListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, false);
  return listener;
}
function addEventCaptureListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, true);
  return listener;
}
  • 事件分为 可代理事件 和 不可代理事件(NonDelegated)
  • 不可代理事件在 completeWork中处理, 目前仅有scroll事件.
  1. registrationNameDependencies: 维护着react事件属性和原生DOM事件名称的映射关系
function setInitialDOMProperties(){
     // registrationNameDependencies 
     // 维护着react事件属性和原生DOM事件名称的映射关系
     // { onScroll: ['scroll'] }
     if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if (propKey === 'onScroll') {
          // 直接绑定到DOM上
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
    }
}
  • 可代理事件在createRoot/listenToAllSupportedEvents中绑定.
function createRoot(container, options) {
  //
  var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
  // 绑定可代理的事件
  listenToAllSupportedEvents(rootContainerElement);
}

function listenToAllSupportedEvents(){
      // allNativeEvents 记录着所有的dom事件名称
      allNativeEvents.forEach(function (domEventName) {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      // 🔥 可以看到 selectionchange 绑定在document上,而不是 root上
      if (domEventName !== 'selectionchange') {
        if (!nonDelegatedEvents.has(domEventName)) {
          //  冒泡阶段监听
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        // 捕获阶段监听
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
}

function listenToNativeEvent(){
  addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
  • react中的事件处理函数

  • 🔥 不同的事件存在不同的事件优先级, 不同的优先级也对应着不同的事件处理函数.

  1. 比如 click对应着 dispatchDiscreteEvent
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
  // 根据事件名称获取事件优先级
  var eventPriority = getEventPriority(domEventName);
  var listenerWrapper;

  switch (eventPriority) {
    case DiscreteEventPriority:
      // click对应着 dispatchDiscreteEvent
      listenerWrapper = dispatchDiscreteEvent;
      break;

    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;

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

  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
  1. dispatchDiscreteEvent/dispatchContinuousEvent都是基于dispatchEvent的,只是对应着不同的更新优先级
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
  var previousPriority = getCurrentUpdatePriority();
  var prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = null;

  try {
    setCurrentUpdatePriority(DiscreteEventPriority);
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

function dispatchContinuousEvent(domEventName, eventSystemFlags, container, nativeEvent) {
  var previousPriority = getCurrentUpdatePriority();
  var prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = null;

  try {
    setCurrentUpdatePriority(ContinuousEventPriority);
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
  if (!_enabled) {
    return;
  }

  {
    dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(domEventName, eventSystemFlags, targetContainer, nativeEvent);
  }
}

事件触发

  • 🔥 通过上面的分析 已经清楚了真正绑定的DOM事件处理函数为 dispatchEvent
  • return_targetInst为 DOM 对应的fiber对象
function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(){
  // 🔥 
  var blockedOn = findInstanceBlockingEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
  if (blockedOn === null) {
    // 
    dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, return_targetInst, targetContainer);
    clearIfContinuousEvent(domEventName, nativeEvent);
    return;
  }
}
// 🔥 事件对象 对应的 fiber对象
var return_targetInst = null

function findInstanceBlockingEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
  // TODO: Warn if _enabled is false.
  return_targetInst = null;
  var nativeEventTarget = getEventTarget(nativeEvent);
  var targetInst = getClosestInstanceFromNode(nativeEventTarget);
  return_targetInst = targetInst; // We're not blocked on anything.
  return null;
}
  • 🔥 dispatchEventForPluginEventSystem
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  //
  var dispatchQueue = [];
  // 1. 收集需要执行的事件处理函数
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // 2. 执行需要执行的事件处理函数
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
  1. 收集事件处理函数, 通过 fiber.return向上收集所有祖先 hostComponent's fiber节点(原生DOM)的事件处理函数.
function extractEvents$5(dispatchQueue, domEventName){

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

function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly, nativeEvent) {
  var captureName = reactName !== null ? reactName + 'Capture' : null;
  var reactEventName = inCapturePhase ? captureName : reactName;
  var listeners = [];
  var instance = targetFiber;
  var lastHostComponent = null; // Accumulate all instances and listeners via the target -> root path.

  while (instance !== null) {
    var _instance2 = instance,
        stateNode = _instance2.stateNode,
        tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)
    // 收集 HostComponent 
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode; // createEventHandle listeners

      if (reactEventName !== null) {
        var listener = getListener(instance, reactEventName);

        if (listener != null) {
          listeners.push(createDispatchListener(instance, listener, lastHostComponent));
        }
      }
    } // If we are only accumulating events for the target, then we don't
    // continue to propagate through the React fiber tree to find other
    // listeners.


    if (accumulateTargetOnly) {
      break;
    } // If we are processing the onBeforeBlur event, then we need to take

    // 🔥 
    instance = instance.return;
  }

  return listeners;
}
  1. 执行事件处理函数, 模拟事件冒泡和捕获的事件执行顺序 执行收集到的事件处理函数.
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;
    }
  }
}