• react事件系统(新版本)


    上一篇文章讲到,react老版本的事件系统,虽然模拟了事件模拟和冒泡,但是其执行的时机,其实都是在冒泡阶段。

    在这里插入图片描述
    react事件处理的俘获阶段实则是在冒泡阶段执行的。而新版本的事件系统则处理了这个问题。如
    在这里插入图片描述
    可以看到react事件处理的俘获事件在俘获的阶段执行了,为什么会在第一个执行呢?因为react17开始,在初始化的时候,就已经向根容器注册了所有的事件了,初始化阶段注册的函数比useEffect注册的时机早。。接下来从18版本的源码开始看看新版本的事件系统跟老版本的区别。

    新版本的区别主要体现在事件绑定和事件触发

    事件绑定

    在新版本的事件系统中,在craeateRoot执行的时候,就会执行listenToAllSupportedEvents一口气向外层容器注册完 全部事件。
    在这里插入图片描述
    先获取了root容器,然后再执行listenToAllSupportedEvents。
    看看listenToAllSupportedEvents如何注册全部事件。

    listenToAllSupportedEvents

    在这里插入图片描述
    这里需要注意几个点

    • 1 allNativeEvents是一个set集合,他保存着81个原生事件。
      在上一篇说过,事件插件在初始化的时候就会注册在这里插入图片描述
      直接调用registerEvents,他不止会收集react事件跟原生事件的依赖,还会收集所有事件。
      在这里插入图片描述
      用set是因为set不会有重复的值。这样当所有插件注册完毕之后,所有原生事件也就收集完毕了。

    • 2 nonDelegatedEvents存放着所有不会冒泡的事件集合。如pause, scroll。

    • 3 如果事件不冒泡,即只执行listenToNativeEvent(domEventName, true, rootContainerElement);,如果冒泡,那么就会执行
      listenToNativeEvent(domEventName, true, rootContainerElement);listenToNativeEvent(domEventName, false, rootContainerElement)
      其实这里就可以看出区别了,第二个参数就是控制是否注册冒泡的,true表示注册俘获事件,false表示注册冒泡事件。

    • 4 listenToNativeEvent函数,他最终调用addTrappedEventListener函数来注册函数。
      在这里插入图片描述

    function addTrappedEventListener(){
    // 判断事件执行的优先级,返回对应监听器。
      let listener = createEventListenerWrapperWithPriority(
        targetContainer,
        domEventName,
        eventSystemFlags,
      );
    ....
    
    if(isCapturePhaseListener){
    	// 注册俘获事件
    	 unsubscribeListener = addEventCaptureListener(
            targetContainer,
            domEventName,
            listener,
          );
    }else {
    	// 注册冒泡事件。
    	  unsubscribeListener = addEventBubbleListener(
            targetContainer,
            domEventName,
            listener,
          );
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这个函数比较重要的就是两点,一点是createEventListenerWrapperWithPriority,他会通过优先级获取listener,也就是最终我们注册到容器上要执行的函数。第二点则是通过isCapturePhaseListener变量(传入给listenToNativeEvent的第二个值),如果是冒泡就注册冒泡事件,否则注册俘获事件。

    createEventListenerWrapperWithPriority

    在这里插入图片描述
    根据不同的优先级获取不同的dispatchEvent函数,最后都会通过bind绑定当前事件的名称。也就是说当我们触发事件的时候,最终执行的都是dispatchEvent或者dispatchDiscreteEvent…函数

    addEventCaptureListener& addEventBubbleListener

    在这里插入图片描述
    他们的本质就是调用容器.addEventListener函数,注册事件了。

    自此,在createRoot初始化的时候,所有事件注册完毕。此时如果触发一次click事件,那么会执行两次dispatchEvent了,一次是俘获阶段,一次是冒泡阶段,这也是跟16版本不同的地方。

    事件触发

    一次click事件的发生,会执行dispatchEvent函数。而该函数最终会执行
    在这里插入图片描述
    batchedUpdates是批量更新的逻辑。主要看看dispatchEventsForPlugins函数。
    在这里插入图片描述
    主要是四个逻辑。

    • 通过getEventTarget,传入原生的事件源,然后找到发生点击的dom
    • 创建一个待更新队列
    • 执行extractEvents收集事件
    • 执行processDispatchQueue消费事件。

    主要看看extractEvents函数
    在这里插入图片描述
    他会执行SimpleEventPlugin.extractEvents。

    function extractEvents(){
     // 获取click对应的react事件onClick
      const reactName = topLevelEventsToReactNames.get(domEventName);
    
     let SyntheticEventCtor = SyntheticEvent; //默认事件源
    
    // swtich case根据事件获取对应的事件源
     // 根据事件类型获取事件源
      switch (domEventName) {
       ...
    
        case 'click':
          SyntheticEventCtor = SyntheticMouseEvent; //click事件源
          break;
       ...
      }
    
    
     // 是否是俘获
      const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
      // 是否冒泡事件
        const accumulateTargetOnly =
          !inCapturePhase &&
          domEventName === 'scroll';
    
      // 获取真正执行的函数
        const listeners = accumulateSinglePhaseListeners(
          targetInst,
          reactName,
          nativeEvent.type,
          inCapturePhase,
          accumulateTargetOnly,
          nativeEvent,
        );
        if (listeners.length > 0) {
          // 有的话就生成事件源
          // Intentionally create event lazily.
          const event = new SyntheticEventCtor(
            reactName,
            reactEventType,
            null,
            nativeEvent,
            nativeEventTarget,
          );
          // push进待更新队列
          dispatchQueue.push({event, listeners});
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    这个函数的重点就是

    • 根据不同的事件获取事件源。
    • 执行accumulateSinglePhaseListeners,获取所有的listener。
    • 生成react的事件源,push进dispatchQueue队列。
    accumulateSinglePhaseListeners

    accumulateSinglePhaseListeners函数会从当前fiber往上遍历直到root,沿途收集所有需要执行的事件。
    在这里插入图片描述
    如图,先判断要收集的名称是俘获还是冒泡,然后通过while循环向上遍历,获取需要执行的函数listener,然后往数组push一个对象,{instance, listener, currentTarget}。最后返回数组。

    此时的dispatchQueue大概长这个样

    {
    event: {...}// react合成的事件源。
    listeners: [{instance: 当前fiber, listener: f(), currentTaget: dom},{...},{...}]
    }
    
    • 1
    • 2
    • 3
    • 4

    最后看如何消费dispatchQueue队列。

    processDispatchQueue

    在这里插入图片描述
    通常情况下,只有一个事件类型,所有dispatchQueue中只有一个元素,
    在这里插入图片描述

    • 通过for循环消费listeners数组,这里俘获的时候,执行顺序是从后往前的,为的是更好模拟俘获顺序。
    • 然后阻止冒泡的逻辑依然是判断event.isPropagationStopped
    • 执行executeDispatch函数,他是最终执行listener函数。

    自此,事件触发阶段完毕。
    一次点击,两次dispatchEvent函数触发。

    • 第一次收集俘获事件,然后执行
    • 第二次冒泡的时候,收集冒泡事件,然后执行。
    总结

    在这里插入图片描述
    (图片来自掘金的《react进阶实践指南》)

    • 新版本的在初始化的时候,就已经绑定事件了。而老版本是在遍历fiber节点的props遇到事件注册才会向容器绑定事件。

    • 其次就是执行时机的不同,在老版本,所有事件不管是俘获还是冒泡,本质就是在冒泡的时候,遍历fiber树,收集俘获和冒泡的事件,形成事件队列,依次执行,以此模拟事件流

    • 而在新版本,一次事件的触发会执行两次dispatchEvent,第一次收集所有的俘获事件执行。第二次收集所有的冒泡事件执行。执行时机与原生的俘获冒泡时机相同。

    • 参考掘金的 《react进阶实践指南》

  • 相关阅读:
    JavaScript 数组去重大揭秘:高手必备技巧一网打尽!
    【rust/esp32】初识slint ui框架并在st7789 lcd上显示
    arduino 复习题
    Fluent计算出现“Floating point exception”时的解决办法
    华为:数据治理方法论
    互联网Java工程师面试题·微服务篇·第一弹
    前端小案例3:Flex弹性布局行内元素宽度自适应
    387.字符串中的第一个唯一字符
    Lua语言编写爬虫程序
    JavaScript——关于JavaScript、在HTML中嵌入JS代码的三种方式、变量
  • 原文地址:https://blog.csdn.net/lin_fightin/article/details/127997561