React事件机制源码解析

2022-04-15 0 1,027
目录
  • 原理简述
  • 源码浅析
    • 委托事件绑定
      • listenToAllSupportedEvents
      • listenToNativeEvent
      • addTrappedEventListener
    • 不需要委托事件绑定
      • setInitialProperties
      • listenToNonDelegatedEvent
    • 事件处理函数
      • createEventListenerWrapperWithPriority
      • dispatchEvent
      • dispatchEventsForPlugins
      • extractEvents
      • accumulateSinglePhaseListeners
      • processDispatchQueue
      • processDispatchQueueItemsInOrder
      • executeDispatch
  • 结语
    • 小思考

    React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的。

    本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关。

    原理简述

    React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身。

    同时,React将事件分为3种类型——discreteEvent、userBlockingEvent、continuousEvent,它们拥有不同的优先级,在绑定事件处理函数时会使用不同的回调函数。

    React事件建立在原生基础上,模拟了一套冒泡和捕获的事件机制,当某一个DOM元素触发事件后,会冒泡到React绑定在root节点的处理函数,通过target获取触发事件的DOM对象和对应的Fiber节点,由该Fiber节点向上层父级遍历,收集一条事件队列,再遍历该队列触发队列中每个Fiber对象对应的事件处理函数,正向遍历模拟冒泡,反向遍历模拟捕获,所以合成事件的触发时机是在原生事件之后的。

    Fiber对象对应的事件处理函数依旧是储存在props里的,收集只是从props里取出来,它并没有绑定到任何元素上。

    源码浅析

    以下源码仅为基础逻辑的浅析,旨在理清事件机制的触发流程,去掉了很多流程无关或复杂的代码。

    委托事件绑定

    这一步发生在调用了ReactDOM.render过程中,在创建fiberRoot的时候会在root节点的DOM元素上监听所有支持的事件。

    function createRootImpl(
      container: Container,
      tag: RootTag,
      options: void | RootOptions,
    ) {
      // ...
      const rootContainerElement =
            container.nodeType === COMMENT_NODE ? container.parentNode : container;
      // 监听所有支持的事件
      listenToAllSupportedEvents(rootContainerElement);
      // ...
    }
    

    listenToAllSupportedEvents

    在绑定事件时,会通过名为allNativeEvents的Set变量来获取对应的eventName,这个变量会在一个顶层函数进行收集,而nonDelegatedEvents是一个预先定义好的Set。

    export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
      allNativeEvents.forEach(domEventName => {
        // 排除不需要委托的事件
        if (!nonDelegatedEvents.has(domEventName)) {
          // 冒泡
          listenToNativeEvent(
            domEventName,
            false,
            ((rootContainerElement: any): Element),
            null,
          );
        }
        // 捕获
        listenToNativeEvent(
          domEventName,
          true,
          ((rootContainerElement: any): Element),
          null,
        );
      });
    }
    

    listenToNativeEvent

    listenToNativeEvent函数在绑定事件之前会先将事件名在DOM元素中标记,判断为false时才会绑定。

    export function listenToNativeEvent(
      domEventName: DOMEventName,
      isCapturePhaseListener: boolean,
      rootContainerElement: EventTarget,
      targetElement: Element | null,
      eventSystemFlags?: EventSystemFlags = 0,
    ): void {
      let target = rootContainerElement;
    	// ...
      // 在DOM元素上储存一个Set用来标识当前元素监听了那些事件
      const listenerSet = getEventListenerSet(target);
      // 事件的标识key,字符串拼接处理了下
      const listenerSetKey = getListenerSetKey(
        domEventName,
        isCapturePhaseListener,
      );
    
      if (!listenerSet.has(listenerSetKey)) {
        // 标记为捕获
        if (isCapturePhaseListener) {
          eventSystemFlags |= IS_CAPTURE_PHASE;
        }
        // 绑定事件
        addTrappedEventListener(
          target,
          domEventName,
          eventSystemFlags,
          isCapturePhaseListener,
        );
        // 添加到set
        listenerSet.add(listenerSetKey);
      }
    }
    

    addTrappedEventListener

    addTrappedEventListener函数会通过事件名取得对应优先级的listener函数,在交由下层函数处理事件绑定。

    这个listener函数是一个闭包函数,函数内能访问targetContainer、domEventName、eventSystemFlags这三个变量。

    function addTrappedEventListener(
      targetContainer: EventTarget,
      domEventName: DOMEventName,
      eventSystemFlags: EventSystemFlags,
      isCapturePhaseListener: boolean,
      isDeferredListenerForLegacyFBSupport?: boolean,
    ) {
      // 根据优先级取得对应listener
      let listener = createEventListenerWrapperWithPriority(
        targetContainer,
        domEventName,
        eventSystemFlags,
      );
    
      if (isCapturePhaseListener) {
        addEventCaptureListener(targetContainer, domEventName, listener);
      } else {
        addEventBubbleListener(targetContainer, domEventName, listener);
      }
    }
    

    addEventCaptureListener函数和addEventBubbleListener函数内部就是调用原生的target.addEventListener来绑定事件了。

    这一步是循环一个存有事件名的Set,将每一个事件对应的处理函数绑定到root节点DOM元素上。

    不需要委托事件绑定

    不需要委托的事件其中也包括媒体元素的事件。

    export const nonDelegatedEvents: Set<DOMEventName> = new Set([
      'cancel',
      'close',
      'invalid',
      'load',
      'scroll',
      'toggle',
      ...mediaEventTypes,
    ]);
    export const mediaEventTypes: Array<DOMEventName> = [
      'abort',
      'canplay',
      'canplaythrough',
      'durationchange',
      'emptied',
      'encrypted',
      'ended',
      'error',
      'loadeddata',
      'loadedmetadata',
      'loadstart',
      'pause',
      'play',
      'playing',
      'progress',
      'ratechange',
      'seeked',
      'seeking',
      'stalled',
      'suspend',
      'timeupdate',
      'volumechange',
      'waiting',
    ];
    

    setInitialProperties

    setInitialProperties方法里会绑定不需要委托的直接到DOM元素本身,也会设置style和一些传入的DOM属性。

    export function setInitialProperties(
      domElement: Element,
      tag: string,
      rawProps: Object,
      rootContainerElement: Element | Document,
    ): void {
      let props: Object;
      switch (tag) {
        // ...
        case 'video':
        case 'audio':
          for (let i = 0; i < mediaEventTypes.length; i++) {
            listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
          }
          props = rawProps;
          break;
        default:
          props = rawProps;
      }
      // 设置DOM属性,如style...
      setInitialDOMProperties(
        tag,
        domElement,
        rootContainerElement,
        props,
        isCustomComponentTag,
      );
    }
    

    switch里会根据不同的元素类型,绑定对应的事件,这里只留下了video元素和audio元素的处理,它们会遍历mediaEventTypes来将事件绑定在DOM元素本身上。

    listenToNonDelegatedEvent

    listenToNonDelegatedEvent方法逻辑和上一节的listenToNativeEvent方法基本一致。

    export function listenToNonDelegatedEvent(
      domEventName: DOMEventName,
      targetElement: Element,
    ): void {
      const isCapturePhaseListener = false;
      const listenerSet = getEventListenerSet(targetElement);
      const listenerSetKey = getListenerSetKey(
        domEventName,
        isCapturePhaseListener,
      );
      if (!listenerSet.has(listenerSetKey)) {
        addTrappedEventListener(
          targetElement,
          domEventName,
          IS_NON_DELEGATED,
          isCapturePhaseListener,
        );
        listenerSet.add(listenerSetKey);
      }
    }
    

    值得注意的是,虽然事件处理绑定在DOM元素本身,但是绑定的事件处理函数不是代码中传入的函数,后续触发还是会去收集处理函数执行。

    事件处理函数

    事件处理函数指的是React中的默认处理函数,并不是代码里传入的函数。

    这个函数通过createEventListenerWrapperWithPriority方法创建,对应的步骤在上文的addTrappedEventListener中。

    createEventListenerWrapperWithPriority

    export function createEventListenerWrapperWithPriority(
      targetContainer: EventTarget,
      domEventName: DOMEventName,
      eventSystemFlags: EventSystemFlags,
    ): Function {
      // 从内置的Map中获取事件优先级
      const eventPriority = getEventPriorityForPluginSystem(domEventName);
      let listenerWrapper;
      // 根据优先级不同返回不同的listener
      switch (eventPriority) {
        case DiscreteEvent:
          listenerWrapper = dispatchDiscreteEvent;
          break;
        case UserBlockingEvent:
          listenerWrapper = dispatchUserBlockingUpdate;
          break;
        case ContinuousEvent:
        default:
          listenerWrapper = dispatchEvent;
          break;
      }
      return listenerWrapper.bind(
        null,
        domEventName,
        eventSystemFlags,
        targetContainer,
      );
    }
    

    createEventListenerWrapperWithPriority函数里返回对应事件优先级的listener,这3个函数都接收4个参数。

    function fn(
      domEventName,
      eventSystemFlags,
      container,
      nativeEvent,
    ) {
      //...
    }
    

    返回的时候bind了一下传入了3个参数,这样返回的函数为只接收nativeEvent的处理函数了,但是能访问前3个参数。

    dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法内部其实都调用的dispatchEvent方法。

    dispatchEvent

    这里删除了很多代码,只看触发事件的代码。

    export function dispatchEvent(
      domEventName: DOMEventName,
      eventSystemFlags: EventSystemFlags,
      targetContainer: EventTarget,
      nativeEvent: AnyNativeEvent,
    ): void {
      // ...
      // 触发事件
      attemptToDispatchEvent(
        domEventName,
        eventSystemFlags,
        targetContainer,
        nativeEvent,
      );
      // ...
    }
    

    attemptToDispatchEvent方法里依然会处理很多复杂逻辑,同时函数调用栈也有几层,我们就全部跳过,只看关键的触发函数。

    dispatchEventsForPlugins

    dispatchEventsForPlugins函数里会收集触发事件开始各层级的节点对应的处理函数,也就是我们实际传入JSX中的函数,并且执行它们。

    function dispatchEventsForPlugins(
      domEventName: DOMEventName,
      eventSystemFlags: EventSystemFlags,
      nativeEvent: AnyNativeEvent,
      targetInst: null | Fiber,
      targetContainer: EventTarget,
    ): void {
      const nativeEventTarget = getEventTarget(nativeEvent);
      const dispatchQueue: DispatchQueue = [];
      // 收集listener模拟冒泡
      extractEvents(
        dispatchQueue,
        domEventName,
        targetInst,
        nativeEvent,
        nativeEventTarget,
        eventSystemFlags,
        targetContainer,
      );
      // 执行队列
      processDispatchQueue(dispatchQueue, eventSystemFlags);
    }
    

    extractEvents

    extractEvents函数里主要是针对不同类型的事件创建对应的合成事件,并且将各层级节点的listener收集起来,用来模拟冒泡或者捕获。

    这里的代码较长,删除了不少无关代码。

    function extractEvents(
      dispatchQueue: DispatchQueue,
      domEventName: DOMEventName,
      targetInst: null | Fiber,
      nativeEvent: AnyNativeEvent,
      nativeEventTarget: null | EventTarget,
      eventSystemFlags: EventSystemFlags,
      targetContainer: EventTarget,
    ): void {
      const reactName = topLevelEventsToReactNames.get(domEventName);
      let SyntheticEventCtor = SyntheticEvent;
      let reactEventType: string = domEventName;
    	// 根据不同的事件来创建不同的合成事件
      switch (domEventName) {
        case 'keypress':
        case 'keydown':
        case 'keyup':
          SyntheticEventCtor = SyntheticKeyboardEvent;
          break;
        case 'click':
        // ...
        case 'mouseover':
          SyntheticEventCtor = SyntheticMouseEvent;
          break;
        case 'drag':
        // ...
        case 'drop':
          SyntheticEventCtor = SyntheticDragEvent;
          break;
        // ...
        default:
          break;
      }
      // ...
      // 收集各层级的listener
      const listeners = accumulateSinglePhaseListeners(
        targetInst,
        reactName,
        nativeEvent.type,
        inCapturePhase,
        accumulateTargetOnly,
      );
      if (listeners.length > 0) {
        // 创建合成事件
        const event = new SyntheticEventCtor(
          reactName,
          reactEventType,
          null,
          nativeEvent,
          nativeEventTarget,
        );
        dispatchQueue.push({event, listeners});
      }
    }
    

    accumulateSinglePhaseListeners

    accumulateSinglePhaseListeners函数里就是在向上层遍历来收集一个列表后面会用来模拟冒泡。

    export function accumulateSinglePhaseListeners(
      targetFiber: Fiber | null,
      reactName: string | null,
      nativeEventType: string,
      inCapturePhase: boolean,
      accumulateTargetOnly: boolean,
    ): Array<DispatchListener> {
      const captureName = reactName !== null ? reactName + 'Capture' : null;
      const reactEventName = inCapturePhase ? captureName : reactName;
      const listeners: Array<DispatchListener> = [];
    
      let instance = targetFiber;
      let lastHostComponent = null;
    
      // 通过触发事件的fiber节点向上层遍历收集dom和listener
      while (instance !== null) {
        const {stateNode, tag} = instance;
        // 只有HostComponents有listener (i.e. <div>)
        if (tag === HostComponent && stateNode !== null) {
          lastHostComponent = stateNode;
    
          if (reactEventName !== null) {
            // 从fiber节点上的props中获取传入的事件listener函数
            const listener = getListener(instance, reactEventName);
            if (listener != null) {
              listeners.push({
                instance,
                listener,
                currentTarget: lastHostComponent,
              });
            }
          }
        }
        if (accumulateTargetOnly) {
          break;
        }
        // 继续向上
        instance = instance.return;
      }
      return listeners;
    }
    

    最后的数据结构如下:

    dispatchQueue的数据结构为数组,类型为[{ event,listeners }]。

    这个listeners则为一层一层收集到的数据,类型为[{ currentTarget, instance, listener }]

    processDispatchQueue

    processDispatchQueue函数里会遍历dispatchQueue。

    export function processDispatchQueue(
      dispatchQueue: DispatchQueue,
      eventSystemFlags: EventSystemFlags,
    ): void {
      const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
      for (let i = 0; i < dispatchQueue.length; i++) {
        const {event, listeners} = dispatchQueue[i];
        processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
      }
    }
    

    dispatchQueue中的每一项在processDispatchQueueItemsInOrder函数里遍历执行。

    processDispatchQueueItemsInOrder

    function processDispatchQueueItemsInOrder(
      event: ReactSyntheticEvent,
      dispatchListeners: Array<DispatchListener>,
      inCapturePhase: boolean,
    ): void {
      let previousInstance;
      // 捕获
      if (inCapturePhase) {
        for (let i = dispatchListeners.length - 1; i >= 0; i--) {
          const {instance, currentTarget, listener} = dispatchListeners[i];
          if (instance !== previousInstance && event.isPropagationStopped()) {
            return;
          }
          executeDispatch(event, listener, currentTarget);
          previousInstance = instance;
        }
      } else {
      // 冒泡
        for (let i = 0; i < dispatchListeners.length; i++) {
          const {instance, currentTarget, listener} = dispatchListeners[i];
          if (instance !== previousInstance && event.isPropagationStopped()) {
            return;
          }
          executeDispatch(event, listener, currentTarget);
          previousInstance = instance;
        }
      }
    }
    

    processDispatchQueueItemsInOrder函数里会根据判断来正向、反向的遍历来模拟冒泡和捕获。

    executeDispatch

    executeDispatch函数里会执行listener。

    function executeDispatch(
      event: ReactSyntheticEvent,
      listener: Function,
      currentTarget: EventTarget,
    ): void {
      const type = event.type || 'unknown-event';
      event.currentTarget = currentTarget;
      listener(event);
      event.currentTarget = null;
    }
    

    结语

    本文旨在理清事件机制的执行,按照函数执行栈简单的罗列了代码逻辑,如果不对照代码看是很难看明白的,原理在开篇就讲述了。

    React的事件机制隐晦而复杂,根据不同情况做了非常多的判断,并且还有优先级相关代码、合成事件,这里都没有一一讲解,原因当然是我还没看~

    平时用React也就写写简单的手机页面,以前老板还经常吐槽加载不够快,那也没啥办法,就对我的工作而言,有没有Cocurrent都是无关紧要的,这合成事件更复杂,完全就是不需要的,不过React的作者们脑洞还是牛皮,要是没看源码我肯定是想不到竟然模拟了一套事件机制。

    小思考

    • 为什么原生事件的stopPropagation可以阻止合成事件的传递?

    这些问题我放以前根本没想过,不过今天看了源码以后才想的。

    • 因为合成事件是在原生事件触发之后才开始收集并触发的,所以当原生事件调用stopPropagation阻止传递后,根本到不到root节点,触发不了React绑定的处理函数,自然合成事件也不会触发,所以原生事件不是阻止了合成事件的传递,而是阻止了React中绑定的事件函数的执行。
    <div 原生onClick={(e)=>{e.stopPropagation()}}>
      <div onClick={()=>{console.log("合成事件")}}>合成事件</div>
    </div>
    

    比如这个例子,在原生onClick阻止传递后,控制台连“合成事件”这4个字都不会打出来了。

    以上就是React事件机制源码解析的详细内容,更多关于React事件机制源码的资料请关注NICE源码其它相关文章!

    免责声明:
    1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

    2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
    如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
    如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

    NICE源码网 JavaScript React事件机制源码解析 https://www.niceym.com/32908.html