React源码解析(四):事件合成

前言

如果按照之前源码解析顺序,这篇应该开始深入 commit 阶段,but,凡事总有但是。react事件合成让我产生了兴趣,所以这一篇我们先来看看这部分是怎么实现,是为了解决什么样的问题。

背景

先抛出一个问题,react事件合成系统是为了解决什么问题来设计的呢?它又是怎么解决这些问题的呢?还有为什么使用这些手段或者技术解问题,是按部就班还是推陈出新?是否还有其他更好的方案呢?其实每一个技术方案的背后都是这些问题,先不展开描述这些,让我们来看看它是如何实现的吧。

实现原理

react 的事件合成系统主要包括以下几个部分:

  1. 事件注册:将事件绑定到根节点。
  2. 事件触发:捕获事件并调用对应的处理函数。
  3. 事件合成:封装原生事件,提供跨浏览器一致的接口。
  4. 事件池:重用事件对象以减少内存开销。

接下来我们来一一解析这些模块:

事件注册

其实在事件绑定到更节点上之前,还有一个事件收集的过程。直接来看代码吧。

js 复制代码
registerSimpleEvents(); // click
registerEvents$2(); // onMouseEnter,onMouseLeave,onPointerEnter,onPointerLeave
registerEvents$1(); // onChange
registerEvents$3(); // onSelect
registerEvents();   // onBeforeInput,onCompositionEnd,onCompositionStart

这些代码的执行顺序,是在引入react就会执行的,也就是说是在 root.render()之前。

registerSimpleEvents

这里先以 registerSimpleEvents() 为例。

js 复制代码
function registerSimpleEvents() {
    for (var i = 0; i < simpleEventPluginEvents.length; i++) {
      var eventName = simpleEventPluginEvents[i];
      var domEventName = eventName.toLowerCase();
      // 小写转大写,click => Click
      var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
      registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
    }
    // ...
  }

simpleEventPluginEvents就是简单事件集合,就是这些事件。

registerSimpleEvent

js 复制代码
function registerSimpleEvent(domEventName, reactName) {
    // 存下事件映射关系,click => onClick
    topLevelEventsToReactNames.set(domEventName, reactName);
    // 
    registerTwoPhaseEvent(reactName, [domEventName]);
  }
function registerTwoPhaseEvent(registrationName, dependencies) {
    registerDirectEvent(registrationName, dependencies);
    registerDirectEvent(registrationName + 'Capture', dependencies);
  }
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]);
    }
  }

这里我们需要注意两个点:

  1. registerTwoPhaseEvent(reactName, [domEventName])这里第二个参数是数组,也就是 reactNamedomEventName,存在一对多的情况,说人话,就是一个react事件会有多个原生事件触发。
  2. allNativeEvents,所有的事件都被收集到这个集合里面了。注册的时候需要用到哦。

registerEvents$1registerEvents$2...就是注册一些抽象事件,也就是[domEventName]参数多个的case。

listenToAllSupportedEvents

js 复制代码
function listenToAllSupportedEvents(rootContainerElement) {
      allNativeEvents.forEach(function (domEventName) {
        // We handle selectionchange separately because it
        // doesn't bubble and needs to be on the document.
        // 我们单独处理selectionchange,因为它不会冒泡,并且需要在文档上。
        if (domEventName !== 'selectionchange') {
          if (!nonDelegatedEvents.has(domEventName)) {
            listenToNativeEvent(domEventName, false, rootContainerElement);
          }

          listenToNativeEvent(domEventName, true, rootContainerElement);
        }
      });
  }

listenToAllSupportedEvents就是所有事件的注册入口,即把allNativeEvents里的所有事件在根节点上添加事件监听器。

其实。我们猜猜也能知道,这里的事件监听是大有玄机的。所有的事件入口都在这里,比如我一个 <button onClick={() => sendCode()}>hahaha。它是如何识别处理的,都在这个监听器上。

listenToNativeEvent

js 复制代码
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
    var eventSystemFlags = 0;
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
  }

这里只是做了层参数抽象。主要是addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener)函数。

addTrappedEventListener

js 复制代码
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
    // 创建监听器
    var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);

    targetContainer = targetContainer;
    var unsubscribeListener;

    // 添加对应的监听器
    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(targetContainer, domEventName, eventSystemFlags)在这里,我们先不展开,等到事件触发的阶段我们再来详细展开这部分。

至此,事件注册的逻辑已经完成了。

事件触发

当我们点击页面或者其他事件触发,react就会执行我们事件注册阶段的listener=createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags)。我们先来看看有哪些调用栈。

dispatchEvent

js 复制代码
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {

dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(domEventName, eventSystemFlags, targetContainer, nativeEvent);

  }

dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay这个函数名可真是太长长长了。那么它具体做了哪些事情呢?我们继续往下看。

大概做了这么几件事:

  1. 检查事件是否被阻塞。
  2. 如果是连续事件,将其加入队列。
  3. 对于需要 hydration 的离散事件,尝试同步 hydration
  4. 根据事件的状态(捕获阶段、阻塞状态等)决定是否派发事件。

其实这个函数名也告诉我们它做了哪些事情了,以前听耗子叔说国内外库函数命名差异大,看了之后才能切身的感受到。
此外,真实的Dom节点和与之对应的fiber是有链接的。

我们接着调用栈往下继续哈。

dispatchEventForPluginEventSystem

js 复制代码
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
    var ancestorInst = targetInst;
    
    // 是否需要将目标fiber更改为不同的祖先
    // ...
    
    // 批次调用
    batchedUpdates(function () {
      return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
    });
  }

核心点在于 目标实例和祖先实例

  • targetInst 是事件的目标实例。
  • ancestorInst 是事件的目标实例或其祖先实例,用于确定事件的冒泡路径

batchedUpdates 这里我们先不深入,主要是优化性能的。

dispatchEventsForPlugins

js 复制代码
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
    var nativeEventTarget = getEventTarget(nativeEvent);
    var dispatchQueue = [];
    // 提取事件
    extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
    // 处理事件
    processDispatchQueue(dispatchQueue, eventSystemFlags);
  }

这里主要包括两个部分,提取事件和处理事件。

extractEvents$5
js 复制代码
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$4 这个函数,具体的逻辑。

extractEvents$4
js 复制代码
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':
        if (getEventCharCode(nativeEvent) === 0) {
          return;
        }
      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.
        // Firefox 在鼠标右键单击时创建点击事件。这将删除不需要的点击事件
        if (nativeEvent.button === 2) {
          return;
        }
        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) {
        // Intentionally create event lazily.
        var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);

        // 维护到队列里面
        dispatchQueue.push({
          event: _event,
          listeners: _listeners
        });
      }
    }
  }

这里面做了这么几件事:

  1. 对不同的事件使用不同合成事件构造器,它主要用于抹平不同浏览器事件对象的差异性。
  2. accumulateSinglePhaseListeners,主要用于收集元素上面的绑定事件监听器(react语法的那种)。
  3. 通过事件构造器实例化得到reactevent,然后将监听器和事件对象维护到dispatchQueue中,以便后续的处理。
accumulateSinglePhaseListeners

我们来看看其内部实现。

js 复制代码
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;
    // 即 currentTarget
    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; 

      // fiber.tag 为 HostComponent(即Dom),才提取事件
      if (tag === HostComponent && stateNode !== null) {
        lastHostComponent = stateNode; 
        // createEventHandle listeners

        if (reactEventName !== null) {
          // 从对应的 fiber 节点属性之中,把react事件提取出来
          var listener = getListener(instance, reactEventName);

          if (listener != null) {
            listeners.push(createDispatchListener(instance, listener, lastHostComponent));
          }
        }
      }
      // 如果我们只为目标积累事件,那么我们就不会继续通过 React 光纤树传播来寻找其他监听器。
      if (accumulateTargetOnly) {
        break;
      } 
      // If we are processing the onBeforeBlur event, then we need to take
      // 返回父节点
      instance = instance.return;
    }

    return listeners;
  }

这里主要是依据targetFiber,通过getListener()方法,把 fiber中的事件提取出来,然后维护到listeners里面。之后返回父节点,继续提取。

processDispatchQueue
js 复制代码
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();
  }

processDispatchQueue()处理 dispatchQueue内的所有提取的事件。

processDispatchQueueItemsInOrder
js 复制代码
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; createRoot
        }
        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;
      }
    }
  }

processDispatchQueueItemsInOrder函数中, 根据捕获(capture)冒泡(bubble)的不同, 采取了不同的遍历方式:

  1. capture阶段事件: 从上至下调用fiber树中绑定的回调函数, 所以倒序遍历dispatchListeners.
  2. bubble阶段事件: 从下至上调用fiber树中绑定的回调函数, 所以顺序遍历dispatchListeners.
executeDispatch
js 复制代码
function executeDispatch(event, listener, currentTarget) {
    var type = event.type || 'unknown-event';
    event.currentTarget = currentTarget;
    invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
    event.currentTarget = null;
  }
  
function invokeGuardedCallbackProd(name, func, context, a, b, c, d, e, f) {
    var funcArgs = Array.prototype.slice.call(arguments, 3);

    try {
      func.apply(context, funcArgs);
    } catch (error) {
      this.onError(error);
    }
  }

  var invokeGuardedCallbackImpl = invokeGuardedCallbackProd;

executeDispatch 最后的触发链路也就是执行的 listener。这个是在成产模式下的执行链路。但是在开发模式下,它的执行链路就比较有意思的,为了能在开发模式更好的开发,做了很多事情,感兴趣的同学可以去看下源码。

总结一下事件触发阶段的几个关键步骤:

  1. Dom元素关联fiber元素.
  2. 事件的提取
  3. 合成事件对象的构造
  4. 派发事件。

总结

那么react合成事件解决了哪些问题呢?

1.跨浏览器兼容性

问题:不同浏览器对事件处理的方式不同,开发者需编写额外代码来兼容。

解决:React 的事件合成系统封装了浏览器原生事件,提供一致的事件接口,开发者无需担心浏览器差异。

  1. 性能优化

问题:直接在 DOM 上绑定大量事件监听器可能导致性能问题。

解决:React 使用事件委托,将事件监听器绑定到文档根节点,按需触发事件处理函数,减少内存占用并提升性能。

  1. 事件池化(Event Pooling

问题:频繁创建和销毁事件对象会增加垃圾回收负担。

解决:React 使用事件池化技术,复用事件对象,减少内存分配和垃圾回收次数,提升性能。

  1. 统一的事件处理

问题:原生事件处理方式复杂,需手动管理事件绑定和解绑。

解决:React 提供声明式的事件绑定方式,简化事件处理逻辑,自动管理事件监听器的绑定和解绑。

  1. 事件对象的封装

问题:原生事件对象包含大量属性和方法,部分不常用或存在兼容性问题。

解决:React 封装了事件对象,提供常用属性和方法,简化事件处理代码。

  1. 组件化事件处理

问题:原生事件处理难以与组件化开发模式结合。

解决:React 的事件合成系统与组件化开发紧密结合,事件处理函数可以直接定义在组件中,提升代码可维护性。

相关推荐
nt110713 分钟前
大模型实现sql生成 --- 能力不足时的retry
前端·langchain·llm
霸王蟹14 分钟前
Vue的性能优化方案和打包分析工具。
前端·javascript·vue.js·笔记·学习·性能优化
绿草在线22 分钟前
路由Vue Router基本用法
前端·javascript·vue.js
Anlici23 分钟前
embedding 搜索功能怎么实现
前端·人工智能
霸王蟹26 分钟前
Pinia-构建用户仓库和持久化插件
前端·vue.js·笔记·ts·pinia·js
iOS阿玮31 分钟前
Apple开发者已入驻微信公众号
前端·app·apple
她的双马尾1 小时前
React组件复用
javascript·react.js·ecmascript
2013编程爱好者1 小时前
React的Hellow React小案例
前端·javascript·react.js
IT、木易1 小时前
React 中的错误边界(Error Boundaries),如何使用它们捕获组件错误
前端·react.js·前端框架
ThinkPet1 小时前
【003安卓开发方案调研】之ReactNative技术开发安卓
android·react native·react.js