react事件系统(新版本)

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,
      );


这个函数比较重要的就是两点,一点是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);
    

这个函数的重点就是

  • 根据不同的事件获取事件源。
  • 执行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,...,...]

最后看如何消费dispatchQueue队列。

processDispatchQueue


通常情况下,只有一个事件类型,所有dispatchQueue中只有一个元素,

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

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

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

总结


(图片来自掘金的《react进阶实践指南》)

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

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

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

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

以上是关于react事件系统(新版本)的主要内容,如果未能解决你的问题,请参考以下文章

Python 3.8 已发布,现在是切换至新版本的好时机吗?

[译]新版 React DevTools 简介

AI大事件 | TensorFlow新版本,Marcus怼深度学习Yann LeCun怼回去

新的 React DevTools 发布![前端动态]

AI大事件 | 特斯拉开发人工智能芯片,智能爆炸论再引热议,Pytorch新版本发布

纯前端表格控件SpreadJS V11.2新版本发布,全面支持React和Vue