好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

React事件机制源码解析

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事件机制源码的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/6948726117591154719

查看更多关于React事件机制源码解析的详细内容...

  阅读:84次