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 的事件合成系统与组件化开发紧密结合,事件处理函数可以直接定义在组件中,提升代码可维护性。

相关推荐
Dontla1 天前
前端状态管理,为什么要状态管理?(React状态管理、zustand)
前端·react.js·前端框架
编程猪猪侠1 天前
前端根据文件后缀名智能识别文件类型的实用函数
前端
宋辰月1 天前
学习react第一天
javascript·学习·react.js
yinuo1 天前
基于 Git Submodule 的代码同步融合方案
前端
伶俜monster1 天前
大模型 “万能接口” MCP 横空出世!打破数据孤岛,重塑 AI 交互新规则
前端·mcp
你听得到111 天前
肝了半个月,我用 Flutter 写了个功能强大的图片编辑器,告别image_cropper
android·前端·flutter
Macbethad1 天前
Typora 精通指南:掌握高效 Markdown 写作的艺术
前端·macos·前端框架
F_Director1 天前
Webpack DLL动态链接库的应用和思考
前端·webpack·性能优化
益达是我1 天前
【element-plus】element-plus升级到v2.11.7,el-tree文字不显示问题
前端·javascript·vue.js·element-plus
社恐的下水道蟑螂1 天前
从 JS 单线程到 Promise:彻底搞懂异步编程的 "同步化" 魔法
前端·javascript