《深入理解react》之事件系统

1,432 阅读8分钟

一、前面的话

在前面的这篇文章中,我们有提到过react通过事件委托的方式将所有的用户事件都代理到了root这个节点上面,因此每当用户触发对应的事件 时实际上是触发了dispatchEvent这个函数,本篇文章我们就来剖析react的事件机制,学习它是如何从dispatchEvent执行到用户注册的自定义事件的呢?

知识回顾

在前面的文章中我们知道了事件也是分优先级的,我们将其称为事件优先级,简单来说就是每一种事件从出生开始就代表了不同的优先级,在react中通过getEventPriority这个函数大体将事件分为以下几种:

离散事件

例如click、input、blur等等

持续触发事件

例如mouseenter、mouseleave、wheel、scroll等

其他事件

例如message等

对应的在根节点上委托的函数实现也不一样,例如

事件类型函数名
离散事件dispatchDiscreteEvent
持续触发事件dispatchContinuousEvent
其他事件dispatchEvent

虽然如此,但是实际上不管任何事件的发生,最终调用的其实都是dispatchEvent,我们只需要关注他的实现就好了,你可以通过查看dispatchContinuousEvent或者dispatchDiscreteEvent的实现来验证这一点

接下来我们就来看看dispatchEvent的实现逻辑

二、源码分析

正常情况下我们的react应用所有的事件都是通过事件代理来完成交互的,当用户点击了某个按钮时这个事件会冒泡到应用节点上,也就是#root节点上触发dispatchEvent,他会收到这么几个参数

domEventName代表的是原生事件名、targetContainer代表的是应用根节点#rootnativeEvent代表的是原生事件对象

image.png

接下来有这么些过程

找到事件源fiber

接下来会紧接着执行findInstanceBlockingEvent,他是用来找寻事件源fiber的,例如假设这个事件是用户点击某个button触发的,那么这个函数就是用来找到这个button所对应在内存中的那个fiber的,我们一起来看一下

function findInstanceBlockingEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent
  ) {
    return_targetInst = null;
    var nativeEventTarget = getEventTarget(nativeEvent); // e.target || e.srcElement
    var targetInst = getClosestInstanceFromNode(nativeEventTarget);

    if (targetInst !== null) {
      ...
    }
    return_targetInst = targetInst;
    return null;
  }

其方法也很简单,就是通过事件源对象获取事件发生的源DOM对象,它们存储在事件源对象的target属性身上,获取到了源DOM对象,在之前的文章中我们有提到过在创建fiber时,fiber对象和DOM对象是相互引用着的,因此他的身上有一根指针指向对应的fiber节点,直接获取即可。

然后将这个fiber节点赋值给全局变量return_targetInst即可,至此fiber寻找成功

事件插件系统

紧接着执行dispatchEventForPluginEventSystem,在他的逻辑中主要是执行这个函数,而且整个过程属于同步的

 function dispatchEventsForPlugins(
     domEventName, // 事件名称
     eventSystemFlags, // 标记,冒泡时是4,捕获时是0
     nativeEvent, // 事件源对象
     targetInst, // 事件发生源的fiber节点
     targetContainer // 根节点#root;
 ) {
    var nativeEventTarget = getEventTarget(nativeEvent); // 获取事件发生的真实DOM对象
    var dispatchQueue = []; // 准备一个队列
    extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags); // 收集事件
    processDispatchQueue(dispatchQueue, eventSystemFlags); // 触发事件
}

通过上面函数的逻辑,我们可以分析出触发事件系统的逻辑是以下几步:

  1. 通过getEventTarget获取事件发生的源DOM对象,如果是Text类型的DOM节点,那么则会获取包裹着它的父节点。
  2. 通过tractEvents$5来抽取事件,换句话说,就是从事件发生源的fiber节点开始一路上向收集所有的事件,来模拟冒泡的行为
  3. 触发事件,既然收集到了这些事件,那么就依次触发他们所对应的事件,也就是用户在nXXX绑定的函数

第一步比较简单我们就不看了,我们直接来看后面的部分

收集事件

实际上收集事件的函数在extractEvent$4中,我们简单看一下它的逻辑

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':
      ...
      case 'click':
        if (nativeEvent.button === 2) {
          return;
        }
      ...
    }

    var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; 
    // 是否是捕获,在18版本中,会经过两次注册,一次是冒泡,一次是捕获,我们只用看冒泡即可。
    {
      var accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll' 
      // 是否只收集目标节点下的事件,众所周知的。
      var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
      // 收集过程
      if (_listeners.length > 0) {
        var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);

        dispatchQueue.push({
          event: _event,
          listeners: _listeners
        });
      }
    }
  }

在上面的实现过程中我们可以知道,react首先会通过原生事件名找到react体系下的事件名,方便后面我们去获取事件,例如click 对应的是 onClick 等等...

然后 accumulateSinglePhaseListeners 来具体进行收集

function accumulateSinglePhaseListeners(
  targetFiber, // 事件源fiber节点
  reactName, // react体系下的事件名 onClick
  nativeEventType, // 原生事件名
  inCapturePhase, // 是否捕获
  accumulateTargetOnly, // 是否只需要收集源节点的函数
  nativeEvent // 事件源对象
) {
    var captureName = reactName !== null ? reactName + 'Capture' : null;
    var reactEventName = inCapturePhase ? captureName : reactName;
    var listeners = [];
    var instance = targetFiber;
    var lastHostComponent = null; 

    while (instance !== null) {
      var _instance2 = instance,
          stateNode = _instance2.stateNode,
          tag = _instance2.tag; // 获取fiber类型

      if (tag === HostComponent && stateNode !== null) { // 所有的事件必须发生在原生DOM节点上,而且这个DOM节点必须存在
        lastHostComponent = stateNode;
        if (reactEventName !== null) {
          var listener = getListener(instance, reactEventName); // 实际上是从fiber节点的props上获取对应的事件函数
          if (listener != null) {
            listeners.push(createDispatchListener(instance, listener, lastHostComponent));
          }
        }
      } 
      if (accumulateTargetOnly) {
        break;
      } 
      instance = instance.return;
    }
    return listeners;
  }

如果我们对这个函数仔细研究一下,就会发现其实逻辑很简单,假设本次触发的事件为点击事件,那么本质上就是从源fiber节点开始一直向上收集对应的onClick函数,将其添加到listeners中,然后再返回,当然不是直接添加的用户定义的onClick,而是使用createDispatchListener包装了一下,它的包装也特别简单:

function createDispatchListener(instance, listener, currentTarget) {
    return {
      instance: instance,
      listener: listener,
      currentTarget: currentTarget
    };
}

截个图大家感受一下

image.png

然后将收集到的事件用一个数组返回

执行事件

经过收集过后就会有一个dispatchQueue,执行事件的时候,需要用到它,假设我们在页面上上具有父子关系的两个元素上分别绑定一个点击事件,那么收集到的dispatchQueue就是这样的

image.png

每一种事件都会有自己的收集列表,然后接下来进行processDispatchQueue

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;
      // 取出每种事件的listeners列表,然后对他们进行处理
      processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
    } 
}

我们现在单看click这一种事件,因此关注processDispatchQueueItemsInOrder即可

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

这个逻辑也是情理之中的,react会根据冒泡和捕获的特点来决定以什么样的顺序来执行收集到的事件列表,他们是通过executeDispatch来调用的

function executeDispatch(
  event, // 这个是react事件对象,也就是将原生事件对象包装了一层
  listener, // 用户的函数
  currentTarget // 事件源真实DOM对象
) {
    var type = event.type || 'unknown-event';
    event.currentTarget = currentTarget;
    invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
    event.currentTarget = null;
}

实际上react是通过invokeGuardedCallbackAndCatchFirstError来真正的调用用户提供的事件函数的,它在开发环境和生产环境的调用逻辑也不一样,这是为了处理错误而区分的,因为大家可以想象一下,这个函数是由用户提供的自定义函数,因此是不安全的,react需要在这个过程中处理好错误,在生产环境下调用的是这个

function invokeGuardedCallbackProd(name, func, context, a, b, c, d, e, f) {
    var funcArgs = Array.prototype.slice.call(arguments, 3);
    try {
      func.apply(context, funcArgs); // 这个func就是用户提供的事件
    } catch (error) {
      this.onError(error);
    }
}

而在开发环境下是通过invokeGuardedCallbackDev来执行的,它能够更好的将错误暴露给开发者,如果你的提供的事件出现了错误,在开发环境下会由更细致的报错,方便开发者进行调试。

好了接下来我们就来总结下,整个事件执行的过程:

假设我们在注册的事件当中存在更新状态的逻辑,实际上这也是常规操作例如:

 const App = () => {
    const [num, setNum] = React.useState(0);

    let f1 = ()=> setNum(num + 1);
    let f2 = ()=> setNum(num + 1);

    return (
      <div  onClick={f1}>
        <button
          onClick={f2}
        >
          num++
        </button>
      </div>
    );
};

当我们点击按钮之后,会经历以下过程:

  1. 原生事件从button冒泡到root根节点,从而触发dispatchEvent
  2. dispatchEvent执行得知是一个click事件
  3. 获取button对应的fiber节点
  4. 从这个fiber节点向上收集所有的onClick事件,将他们添加到一个容器中
  5. 根据这个容器里的函数,决定以什么样的顺序一一执行
  6. 区分开发环境和生产环境的执行逻辑
  7. 真正的执行
  8. 由于执行了函数,触发了setState,进入更新调度流程....

三、最后的话

好啦,今天我们学习了react中的事件机制,从中我们可以看出当我们在与react应用程序进行交互时中间居然还发生了这么多事情,这篇文章适合在 《深入理解react》之初始化流程 之后阅读,体验会更佳!

后面的文章我们会深入剖析react的源码,学习react的设计思想,如果你也对react相关技术感兴趣请关注我的《深入理解react》专栏,我们一起进步,有帮助的话希望朋友点个赞支持下,多谢多谢!