从ReactDOM.createRoot开始,了解react事件合成系统

前言

为什么要写这个

  1. 看到过很多理论文章讲了react事件系统但是一直没看到一篇文章从源码一步一步看下去,感觉很不爽
  2. react已经很久没有更新了,这个时候写文章大概率不担心过几天就文章过时了嘿嘿。

看文章之前我们需要先知道几个前置知识

  1. 事件委托,就是利用事件冒泡的特性,将事件处理程序绑定到一个父元素上,以代理所有子元素上的事件-> 具体可问gpt
  2. react在17版本开始将事件委托给root而不是document -> 所以我们可以从createRoot看到委托全过程
  3. 事件分为两个阶段,分别是冒泡和捕获,react并没有真实的将事件绑定到dom,所以冒泡和捕获的特性是react模拟的。

后续可能会大量粘贴源码,会尽量给到注释和解释。请耐心阅读

从createRoot开始

先从createRoot开始看代码,我们删除一些没用的error以及warning提示代码如下,除了一些参数设置,就干了两件事情 创建fiberRoot和 listenToAllSupportedEvents,fiber相关内容暂时不关心我们去看listenToAllSupportedEvents

JavaScript 复制代码
function createRoot(container, options) {

  var isStrictMode = false;
  var concurrentUpdatesByDefaultOverride = false;
  var identifierPrefix = '';
  var onRecoverableError = defaultOnRecoverableError;

  if (options !== null && options !== undefined) {
    if (options.unstable_strictMode === true) {
      isStrictMode = true;
    }

    if (options.identifierPrefix !== undefined) {
      identifierPrefix = options.identifierPrefix;
    }

    if (options.onRecoverableError !== undefined) {
      onRecoverableError = options.onRecoverableError;
    }

    if (options.transitionCallbacks !== undefined) {
      transitionCallbacks = options.transitionCallbacks;
    }
  }
   // createContainer
  var root = createContainer(container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
  // 其实就是给fiber设置了某个属性
  markContainerAsRoot(root.current, container);
  var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
  listenToAllSupportedEvents(rootContainerElement);
  return new ReactDOMRoot(root);
}

下面是listenToAllSupportedEvents 代码,注意了这里给root绑定了两个事件,分别用于委托捕获的和冒泡的事件。 这里这里有个变量叫做allNativeEvents 看样子是个全局变量,我们需要找到它在哪里

JavaScript 复制代码
function listenToAllSupportedEvents(rootContainerElement) {
  // 开始注册监听事件!important 这里是react事件系统
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    // 注意这里的allNativeEvents是在哪里设置的
    // 在registerDirectEvent方法中设置
    allNativeEvents.forEach(function (domEventName) {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      if (domEventName !== 'selectionchange') {
         // 除了nonDelegatedEvents 其余所有的event都绑定了两次一次capture一次noCapture
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }

        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    // 特殊处理,需要将selectionchange 添加到document上面
    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);
      }
    }
  }
}

看看registerDirectEvent

往上找可以找到 registerDirectEvent -> registerTwoPhaseEvent -> registerEvents$2 & registerEvents$1 & registerEvents$3 & registerEvents 。这几个函数都是立即执行的,作用是将react事件名称和原生事件名称做一一对应,比如 原生click事件 对应react的onClick事件,react的onClickCapture对应的 click事件的捕获状态。如下代码所示

JavaScript 复制代码
// 原生名称和react事件名称之间的映射Map
var topLevelEventsToReactNames = new Map();

// 用于记录已经注册过的react事件名称和原生事件名称 比如 onClick -> click onClickCapture -> click
var registrationNameDependencies = {};

// 用于记录已经注册的react事件名称及其lowcase对关系
var possibleRegistrationNames = {}
// 因为每一个事件都是有两种捕获方式
// 冒泡和捕获,默认为冒泡,但是也需要提供捕获事件功能,所以这里注册事件的时候有一个Capture,
// 那么就是在react中,默认的onClick是冒泡的,而 onClickCapture则可以注册一个捕获的事件
function registerTwoPhaseEvent(registrationName, dependencies) {
  registerDirectEvent(registrationName, dependencies);
  registerDirectEvent(registrationName + 'Capture', dependencies);
}
// 方法中简历了已注册的react事件的lowcase和名称对应关系
// 并且记录了所有有记载的原生事件
// 请注意,这里的dependencies是一个数组,因为会有onChange和onSelect这种奇葩react事件,会有一对多原生事件的情况
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]);
  }
}

function registerSimpleEvent(domEventName, reactName) {
  topLevelEventsToReactNames.set(domEventName, reactName);
  registerTwoPhaseEvent(reactName, [domEventName]);
}

// 注册事件名称,这一步是做了事件名称映射,比如click事件,在react中名称为onclick
// 这里是一些简单事件注册 - -其实就是满足 名称从 click -> onClick 转换的一些事件
function registerSimpleEvents() {
  for (var i = 0; i < simpleEventPluginEvents.length; i++) {
    var eventName = simpleEventPluginEvents[i];
    var domEventName = eventName.toLowerCase();
    // 将原生的eventName转换为onAbort这种形式
    var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
    registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
  } // Special cases where event names don't match.

  // 特殊处理一些eventName
  registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
  registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
  registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
  registerSimpleEvent('dblclick', 'onDoubleClick');
  registerSimpleEvent('focusin', 'onFocus');
  registerSimpleEvent('focusout', 'onBlur');
  registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}

// 以下几个特殊事件是不需要注册捕获的capture并且还有一对多的特性
function registerEvents$2() {
  registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
  registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
  registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
  registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}

function registerEvents$3() {
  registerTwoPhaseEvent('onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']);
}

// 非常特殊的onChange和onSelect,很多个事件都会触发onChange,同时
function registerEvents$1() {
  registerTwoPhaseEvent('onChange', ['change', 'click', 'focusin', 'focusout', 'input', 'keydown', 'keyup', 'selectionchange']);
}

function registerEvents() {
  registerTwoPhaseEvent('onBeforeInput', ['compositionend', 'keypress', 'textInput', 'paste']);
  registerTwoPhaseEvent('onCompositionEnd', ['compositionend', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
  registerTwoPhaseEvent('onCompositionStart', ['compositionstart', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
  registerTwoPhaseEvent('onCompositionUpdate', ['compositionupdate', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
}

registerSimpleEvents();
registerEvents$2();
registerEvents$1();
registerEvents$3();
registerEvents();

在这里能看到一些比较有意思的是,我们可以看到某些事件其实对应了多个原生事件,比如onChange和onSelect,也就是说onChange对应的8个原生事件,任意一个触发都会触发react的onChange。

回到listenToNativeEvent

我们看listenToNativeEvent的调用链,listenToNativeEvent -> addTrappedEventListener -> createEventListenerWrapperWithPriority -> addEventCaptureListenerWithPassiveFlag & addEventCaptureListener & addEventBubbleListenerWithPassiveFlag & addEventBubbleListener。

其中最后几个addEvent都没啥好看的就是往root绑定一个事件,调用了addEventListener而已。我们主要看前面几个

javascript 复制代码
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
  var eventSystemFlags = 0;

  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  
  addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
javascript 复制代码
// 学习一下如何检测是否支持passive
var passiveBrowserEventsSupported = false; 
 try {
    var options = {}; 

    Object.defineProperty(options, 'passive', {
      get: function () {
        passiveBrowserEventsSupported = true;
      }
    });
    window.addEventListener('test', options, options);
    window.removeEventListener('test', options, options);
  } catch (e) {
    passiveBrowserEventsSupported = false;
  }
  
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);

  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
    // 这里对于passive做了处理,浏览器默认在document上面的滚动监听是 passive:true的,
    // 也就是说滚动不会被preventDefault() 无法阻止。
    
    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);
    }
  }
}

关键函数为 createEventListenerWrapperWithPriority,这里面给不同类型的事件设置了不同的优先级,优先级是调度系统相关,我们先不管,我们只需要知道,最终我们给root 所有的原生dom事件都绑定了一个事件,事件名称叫dispatchEvent。

ini 复制代码
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
  var eventPriority = getEventPriority(domEventName);
  var listenerWrapper;

  switch (eventPriority) {
      // dispatchDiscreteEvent 和 dispatchContinuousEvent最终还是调用了dispatchEvent只不过设置了不同的优先级而已,不需要看这两个函数。
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;

    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;

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

  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}

dispatchEvent在干啥

我们继续看 dispatchEvent调用链 dispatchEvent -> dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay -> dispatchEventForPluginEventSystem -> dispatchEventsForPlugins。其中dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay有一段判断系统阻塞逻辑,dispatchEventForPluginEventSystem有一段逻辑用于处理存在createPortal情况下有多个root的情况,我们不需要管我们直接看dispatchEventsForPlugins

scss 复制代码
function getEventTarget(nativeEvent) {
  // 获取target dom的方法
  var target = nativeEvent.target || nativeEvent.srcElement || window; // Normalize SVG <use> element events #4963

  if (target.correspondingUseElement) {
    target = target.correspondingUseElement;
  } // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
  // @see http://www.quirksmode.org/js/events_properties.html

   // 
  return target.nodeType === TEXT_NODE ? target.parentNode : target;
}
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var dispatchQueue = [];
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}


function extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
  extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  var shouldProcessPolyfillPlugins = (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0; 
  if (shouldProcessPolyfillPlugins) {
    extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
    extractEvents$1(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
    extractEvents$3(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
    extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
  }
}

这里我们找到了我们的核心处理逻辑 extractEvents 和 processDispatchQueue。这里我们可以在 extractEvents$4方法中直接找到一个方法 accumulateSinglePhaseListeners,用于收集所有注册的click。如下所示,这里有一个重点函数 getListener,当我们知道了目标dom元素时,我们如何拿到它注册的事件回调方法呢? 直接访问element[internalPropsKey],(直接找 __reactProps 开头的字段)每一个element都会有一个internalPropsKey用于存储element对应的组件参数

kotlin 复制代码
function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
  var reactName = topLevelEventsToReactNames.get(domEventName);

  if (reactName === undefined) {
    return;
  }

  var SyntheticEventCtor = SyntheticEvent;
  var reactEventType = 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) === 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;
  }

  var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

  {
    // 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.
    var 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';

    var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);

    if (_listeners.length > 0) {
      // 这个event是用于传入回调事件的,其实就是将原生event类型加了几个字段而已
      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>)

    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode; // createEventHandle listeners


      if (reactEventName !== null) {
       // 请注意这里的的getListener
        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;
} 

这段代码很明显,我们从目标元素一直往上遍历,遍历到root位置,收集沿途所有的onClick事件并把它push到一个数组中。然后我们生成一个用于传入onClick的参数。 最后我们看到从processDispatchQueue开始processDispatchQueue -> processDispatchQueueItemsInOrder -> executeDispatch -> invokeGuardedCallbackAndCatchFirstError -> invokeGuardedCallbackImpl -> invokeGuardedCallbackImpl

ini 复制代码
function invokeGuardedCallbackProd(name, func, context, a, b, c, d, e, f) {
  var funcArgs = Array.prototype.slice.call(arguments, 3);

  try {
      // context一直都是一个undefined
    func.apply(context, funcArgs);
  } catch (error) {
    this.onError(error);
  }
}
var invokeGuardedCallbackImpl = invokeGuardedCallbackProd;
function invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {
  hasError = false;
  caughtError = null;
  invokeGuardedCallbackImpl$1.apply(reporter, arguments);
}
function invokeGuardedCallbackAndCatchFirstError(name, func, context, a, b, c, d, e, f) {
  invokeGuardedCallback.apply(this, arguments);

  if (hasError) {
    var error = clearCaughtError();

    if (!hasRethrowError) {
      hasRethrowError = true;
      rethrowError = error;
    }
  }
}

结尾

到此为止,已经完全弄清楚了react的事件委托的总流程

  1. 给root挂上捕获和冒泡的两个事件
  2. 点击元素后通过元素上面的__reactProps获取当前props找到对应的事件名称,然后在当前fiber节点不断向上访问,一直遍历到root收集沿途所有的事件
  3. 根据事件是否冒泡决定事件数组的执行顺序。

后续会了解一下react的调度机制,和react的虚拟dom对比机制。

这里react的调度机制主要依赖于 scheduler库,先看看scheduler如何使用,然后再看看scheduler在react中有什么用。

在了解完前置知识后,同样的整个分析会从 root.render()方法开始。

相关推荐
September_ning4 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人4 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0014 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking7 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫8 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull12 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet1 天前
那总结下来,react就是落后了
前端·react.js
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router