浅谈React17事件机制

3,746 阅读5分钟

前言

先看官网对17版本的介绍:

React v17 中,React 不会再将事件处理添加到 document 上,而是将事件处理添加到渲染 React 树的根 DOM 容器中:

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

在 React 16 及之前版本中,React 会对大多数事件进行 document.addEventListener() 操作。React v17 开始会通过调用 rootNode.addEventListener() 来代替。

官方还提供了一幅示意图: react17事件绑定

来看下react17输出的页面事件绑定结果是什么样子的: html.png 再看下html: root.png 可以看到事件都在root节点监听了而且监听了全部的事件。下面我们就开始剖析17的事件机制。

事件机制

可以简单概括为事件绑定处理合成事件收集事件响应链路触发事件回调函数

事件绑定

函数调用关系:

graph TD
listenToAllSupportedEvents --> listenToNativeEvent --> addTrappedEventListener

下面我们再具体看一下每个函数主要做了什么:

listenToAllSupportedEvents

function listenToAllSupportedEvents(rootContainerElement) {
  // ...
  // allNativeEvents是所有的native事件
  allNativeEvents.forEach(function (domEventName) {
    // nonDelegatedEvents中的事件都是不能冒泡的元素,如果该事件不能冒泡,就只绑定捕获阶段,否则两个阶段都绑定
    if (!nonDelegatedEvents.has(domEventName)) {
      listenToNativeEvent(domEventName, false, rootContainerElement, null);
    }

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

listenToNativeEvent

function listenToNativeEvent(domEventName, isCapturePhaseListener, rootContainerElement, targetElement) {
  // ...
  addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
  // ...
}

addTrappedEventListener

function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
  // 根据事件名,创建优先级不同的监听器
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); 
  // 绑定监听器
  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);
    }
  }
}

对于不向上冒泡的事件如何处理?

非常简单,直接在绑定对应事件的dom节点上监听就完事了,涉及到的代码如下:

function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
  // ...
  switch (tag) {
    case 'dialog':
      listenToNonDelegatedEvent('cancel', domElement);
      listenToNonDelegatedEvent('close', domElement);
      break;

    case 'iframe':
    case 'object':
    case 'embed':
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the load event.
      listenToNonDelegatedEvent('load', domElement);
      break;
      // ...
  }
  // ...
} // Calculate the diff between the two objects.

其中listenToNonDelegatedEvent方法会去调用addTrappedEventListener方法完成事件绑定。最后效果如下:

Screen Shot 2021-06-08 at 12.13.20 AM.png

Screen Shot 2021-06-08 at 12.13.34 AM.png

处理合成事件

调用dispatchEventsForPlugins方法,对native事件进行合成,网上相关的文章很多,这里就不多说了。涉及到的核心方法如下:

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  // 事件对象的合成,收集事件到执行路径上
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // 执行收集到的组件中真正的事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

dispatchQueue是一个数组,每一项包含了一个合成事件及其该合成事件对应的回调函数(listeners),extractEvents负责合成事件并调用相关的方法来收集事件响应链路,processDispatchQueue负责执行事件的回调函数

收集事件响应链路

该阶段会从事件发生源对应的dom节点对应的fiber节点开始,逐一遍历父fiber节点直到根fiber节点,寻找fiber节点props中对应事件函数,比如触发了onClick事件,就在fiber节点的props中找onClick,如果有对应的事件名及其函数,那么就按照顺序收集起来。核心方法:accumulateSinglePhaseListenersaccumulateTwoPhaseListeners。下面以accumulateSinglePhaseListeners为例

function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly) {
  var captureName = reactName !== null ? reactName + 'Capture' : null;
  // 根据一开始设定的事件监听阶段,设置不同的事件名,以onClick为例,reactName是onClick captureName是onClickCapture
  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, // stateNode是fiber节点对应的dom节点
        tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)

    // react只处理在dom节点上绑定的事件
    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));
        }
      }
    }
    
    // ...
    // 向上找
    instance = instance.return;
  }

  return listeners;
}
function getListener(inst, registrationName) {
  var stateNode = inst.stateNode;
  // ...
  
  // 获得dom节点对应的fiber节点上的props
  var props = getFiberCurrentPropsFromNode(stateNode);
  // 获取同名的事件回调函数
  var listener = props[registrationName];

  // ...

  return listener;
}

以下面的组件为例:

function App() {
  return (
    <div className="App">
      <header onClick={() => { console.log('header') }} className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <code onClick={() => { console.log('code') }}>src/App.js</code>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

最后收集到的listeners如下: listeners.png dipatchQueue如下: dipatchQueue.png

触发事件回调函数

在收集事件响应链路阶段,已经收集到了含有事件监听器的一条链路,按照收集顺序执行即从触发节点到根节点,就是模拟了冒泡,反过来就是捕获。核心方法processDispatchQueue, processDispatchQueueItemsInOrder

function processDispatchQueue(dispatchQueue, eventSystemFlags) {
  var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  // 遍历dispatchQueue并调用processDispatchQueueItemsInOrder
  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();
}
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;
    }
  }
}

到此,我们就基本分析完了react17的事件机制,下面做一下总结。

总结

  • react17会在rootContainer上监听所有事件的冒泡和捕获阶段,不能冒泡的事件会直接在对应的dom节点上直接绑定
  • dom节点上不会绑定事件监听器(除了几种特殊情况)
  • 代码element中的事件回调函数保存在fiber节点的props中
  • 监听到事件后,事件会在rootContainer上处理捕获和冒泡阶段(不能冒泡的事件在对应的dom节点上处理),经过函数调用,最后会调用dispatchEventsForPlugins将native事件处理成合成事件,并会从触发节点的fiber节点开始向上搜寻监听相同事件的fiber节点(不能冒泡的事件不会向上搜索),将props中的回调函数放到一个数组里等待执行
  • 最后根据是冒泡事件还是捕获事件,决定执行顺序 有描述错误的地方希望大佬看到可以指出来,谢谢~