blog icon indicating copy to clipboard operation
blog copied to clipboard

React合成事件源码学习

Open rudyxu1102 opened this issue 4 years ago • 0 comments

前言

本次阅读的React源码是v17.0.1,研究一下React的事件机制

疑问

下面是笔者对合成事件比较好奇的地方。

  • React在哪个阶段将事件委托根节点?
  • React的冒泡和捕获机制是如何模拟的?
  • React事件对象是怎么复用的?

示例demo

class About extends Component {
    constructor () {
        super()
    }

    handleChildClick (e) {
        console.log('child click', e)
    }

    handleParentClick (e) {
        console.log('parent click', e)
    }

    render() {
        return (
            <div onClick={(e) => this.handleParentClick(e)}>
                <button onClick={(e) => this.handleChildClick(e)}>click</button>
            </div>
        );
    }
}

React在哪个阶段将事件委托根节点?

执行ReactDom.render的时候,会创建根节点,同时调用listenToAllSupportedEvents,在根节点注册常用的原生事件。根结点的click事件就是在这个时候注册的,会在根节点绑定click的冒泡事件和捕获事件,两个事件绑定的方法都是dispatchDiscreteEvent

注意:在 React 17 中,React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中,因为页面中可能会有多个React版本,将事件委托在document无法实现嵌套树结构的事件机制。

function listenToAllSupportedEvents(rootContainerElement) {
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    allNativeEvents.forEach(function (domEventName) {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      if (domEventName !== 'selectionchange') {
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }

        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    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);
      }
    }
  }
}

如果是onScroll等不常用的事件,会在执行completeWork方法初始化dom节点的时候将事件委托到根节点。

function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
  
    case HostComponent:
      {
        popHostContext(workInProgress);
        var rootContainerInstance = getRootHostContainer();
        var type = workInProgress.type;
        // 更新的时候
        if (current !== null && workInProgress.stateNode != null) {
          updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

          if (current.ref !== workInProgress.ref) {
            markRef$1(workInProgress);
          }
        } else {
          if (!newProps) {
            if (!(workInProgress.stateNode !== null)) {
              {
                throw Error( "We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue." );
              }
            } // This can happen when we abort work.


            return null;
          }

          var currentHostContext = getHostContext(); /

          var _wasHydrated = popHydrationState(workInProgress);

        var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance; 

        // 执行finalizeInitialChildren方法完成事件委托
        if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
            markUpdate(workInProgress);
        }

          if (workInProgress.ref !== null) {
            // If there is a ref on a host node we need to schedule a callback
            markRef$1(workInProgress);
          }
        }

        return null;
      }

  
  }
}

finalizeInitialChildren放内部会调用setInitialProperties方法,执行listenToNonDelegatedEvent完成事件委托。

function listenToNonDelegatedEvent(domEventName, targetElement) {

  var isCapturePhaseListener = false;
  var listenerSet = getEventListenerSet(targetElement);
  var listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);

  if (!listenerSet.has(listenerSetKey)) {
    addTrappedEventListener(targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener);
    listenerSet.add(listenerSetKey);
  }
}

冒泡和捕获机制

react绑定的事件如果想区分是在冒泡还是捕获阶段执行,通过事件函数后缀Capture区别。比如onClick在冒泡阶段执行, onClickCapture发生在捕获阶段。

在示例demo中,点击按钮首先会触发根节点click的捕获事件,然后触发根节点的冒泡事件。因为demo中按钮绑定的是冒泡事件,所以下面我们主要看一下冒泡事件的模拟流程。

根节点冒泡事件绑定的方法dispatchDiscreteEvent。然后通过nativeEvent.target或者nativeEvent.srcElement找到事件触发的位置为button,并找到对应的fiber节点。

dispatchEventForPluginEventSystem方法中,从事件触发的fiber节点向上查找匹配的根节点。

function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var ancestorInst = targetInst;

  if ((eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 && (eventSystemFlags & IS_NON_DELEGATED) === 0) {
    var targetContainerNode = targetContainer; // If we are using the legacy FB support flag, we

    if (targetInst !== null) {
      // The below logic attempts to work out if we need to change
      // the target fiber to a different ancestor. We had similar logic
      // in the legacy event system, except the big difference between
      // systems is that the modern event system now has an event listener
      // attached to each React Roo and React Portal Root. Together,
      // the DOM nodes representing these roots are the "rootContainer".
      // To figure out which ancestor instance we should use, we traverse
      // up the fiber tree from the target instance and attempt to find
      // root boundaries that match that of our current "rootContainer".
      // If we find that "rootContainer", we find the parent fiber
      // sub-tree for that root and make that our instance.
      var node = targetInst;

      mainLoop: while (true) {
        if (node === null) {
          return;
        }

        var nodeTag = node.tag;

        if (nodeTag === HostRoot || nodeTag === HostPortal) {
          var container = node.stateNode.containerInfo;

          if (isMatchingRootContainer(container, targetContainerNode)) {
            break;
          }

          if (nodeTag === HostPortal) {
            // The target is a portal, but it's not the rootContainer we're looking for.
            // Normally portals handle their own events all the way down to the root.
            // So we should be able to stop now. However, we don't know if this portal
            // was part of *our* root.
            var grandNode = node.return;

            while (grandNode !== null) {
              var grandTag = grandNode.tag;

              if (grandTag === HostRoot || grandTag === HostPortal) {
                var grandContainer = grandNode.stateNode.containerInfo;

                if (isMatchingRootContainer(grandContainer, targetContainerNode)) {
                  // This is the rootContainer we're looking for and we found it as
                  // a parent of the Portal. That means we can ignore it because the
                  // Portal will bubble through to us.
                  return;
                }
              }

              grandNode = grandNode.return;
            }
          } // Now we need to find it's corresponding host fiber in the other
          // tree. To do this we can use getClosestInstanceFromNode, but we
          // need to validate that the fiber is a host instance, otherwise
          // we need to traverse up through the DOM till we find the correct
          // node that is from the other tree.


          while (container !== null) {
            var parentNode = getClosestInstanceFromNode(container);

            if (parentNode === null) {
              return;
            }

            var parentTag = parentNode.tag;

            if (parentTag === HostComponent || parentTag === HostText) {
              node = ancestorInst = parentNode;
              continue mainLoop;
            }

            container = container.parentNode;
          }
        }

        node = node.return;
      }
    }
  }

  batchedEventUpdates(function () {
    return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
  });
}

执行dispatchEventsForPlugins方法, 开始模拟事件冒泡。

function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var dispatchQueue = [];
  // 找到所有绑定了相应事件的节点,生成合成事件实例,并放入dispatchQueue队列中
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // 执行dispatchQueue队列中的绑定的方法
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents$5方法中通过从触发事件的fiber节点,向上查找所有绑定了click冒泡事件的fiber节点,并生成合成事件放入队列中。_listeners中会依次放入button和div两个事件执行对象。

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

最后在processDispatchQueue执行dispatchQueue队列中的事件绑定方法。

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

React事件对象是如何复用的?

很遗憾,React17已经去除了事件池。因为复用了事件对象,在某些情况会导致崩溃。

function handleChange(e) {
  setData(data => ({
    ...data,
    // This crashes in React 16 and earlier:
    text: e.target.value
  }));
}

这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用 e.persist() 才能正确的使用该事件,或者正确读取需要的属性。

合成事件构造函数。不再使用事件池,persist方法因此也变成了一个空函数。

  function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;

    for (var _propName in Interface) {
      if (!Interface.hasOwnProperty(_propName)) {
        continue;
      }

      var normalize = Interface[_propName];

      if (normalize) {
        this[_propName] = normalize(nativeEvent);
      } else {
        this[_propName] = nativeEvent[_propName];
      }
    }

    var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;

    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }

    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  _assign(SyntheticBaseEvent.prototype, {
    preventDefault: function () {
      this.defaultPrevented = true;
      var event = this.nativeEvent;

      if (!event) {
        return;
      }

      if (event.preventDefault) {
        event.preventDefault(); // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.returnValue !== 'unknown') {
        event.returnValue = false;
      }

      this.isDefaultPrevented = functionThatReturnsTrue;
    },
    stopPropagation: function () {
      var event = this.nativeEvent;

      if (!event) {
        return;
      }

      if (event.stopPropagation) {
        event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.cancelBubble !== 'unknown') {
        // The ChangeEventPlugin registers a "propertychange" event for
        // IE. This event does not support bubbling or cancelling, and
        // any references to cancelBubble throw "Member not found".  A
        // typeof check of "unknown" circumvents this issue (and is also
        // IE specific).
        event.cancelBubble = true;
      }

      this.isPropagationStopped = functionThatReturnsTrue;
    },

    /**
     * We release all dispatched `SyntheticEvent`s after each event loop, adding
     * them back into the pool. This allows a way to hold onto a reference that
     * won't be added back into the pool.
     */
    persist: function () {// Modern event system doesn't use pooling.
    },

    /**
     * Checks if this event should be released back into the pool.
     *
     * @return {boolean} True if this should not be released, false otherwise.
     */
    isPersistent: functionThatReturnsTrue
  });

参考链接

rudyxu1102 avatar Nov 23 '20 15:11 rudyxu1102