React事件机制源码浅析 #116
zhangyu1818
announced in
zh-cn
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
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元素上监听所有支持的事件。listenToAllSupportedEvents
在绑定事件时,会通过名为
allNativeEvents的Set变量来获取对应的eventName,这个变量会在一个顶层函数进行收集,而nonDelegatedEvents是一个预先定义好的Set。listenToNativeEvent
listenToNativeEvent函数在绑定事件之前会先将事件名在DOM元素中标记,判断为false时才会绑定。addTrappedEventListener
addTrappedEventListener函数会通过事件名取得对应优先级的listener函数,在交由下层函数处理事件绑定。这个
listener函数是一个闭包函数,函数内能访问targetContainer、domEventName、eventSystemFlags这三个变量。addEventCaptureListener函数和addEventBubbleListener函数内部就是调用原生的target.addEventListener来绑定事件了。这一步是循环一个存有事件名的
Set,将每一个事件对应的处理函数绑定到root节点DOM元素上。不需要委托事件绑定
不需要委托的事件其中也包括媒体元素的事件。
这些事件绑定发生在
completeWork阶段,最后会执行setInitialProperties方法。setInitialProperties
setInitialProperties方法里会绑定不需要委托的直接到DOM元素本身,也会设置style和一些传入的DOM属性。switch里会根据不同的元素类型,绑定对应的事件,这里只留下了video元素和audio元素的处理,它们会遍历mediaEventTypes来将事件绑定在DOM元素本身上。listenToNonDelegatedEvent
listenToNonDelegatedEvent方法逻辑和上一节的listenToNativeEvent方法基本一致。值得注意的是,虽然事件处理绑定在DOM元素本身,但是绑定的事件处理函数不是代码中传入的函数,后续触发还是会去收集处理函数执行。
事件处理函数
事件处理函数指的是React中的默认处理函数,并不是代码里传入的函数。
这个函数通过
createEventListenerWrapperWithPriority方法创建,对应的步骤在上文的addTrappedEventListener中。createEventListenerWrapperWithPriority
createEventListenerWrapperWithPriority函数里返回对应事件优先级的listener,这3个函数都接收4个参数。返回的时候
bind了一下传入了3个参数,这样返回的函数为只接收nativeEvent的处理函数了,但是能访问前3个参数。dispatchDiscreteEvent方法和dispatchUserBlockingUpdate方法内部其实都调用的dispatchEvent方法。dispatchEvent
这里删除了很多代码,只看触发事件的代码。
attemptToDispatchEvent方法里依然会处理很多复杂逻辑,同时函数调用栈也有几层,我们就全部跳过,只看关键的触发函数。dispatchEventsForPlugins
dispatchEventsForPlugins函数里会收集触发事件开始各层级的节点对应的处理函数,也就是我们实际传入JSX中的函数,并且执行它们。extractEvents
extractEvents函数里主要是针对不同类型的事件创建对应的合成事件,并且将各层级节点的listener收集起来,用来模拟冒泡或者捕获。这里的代码较长,删除了不少无关代码。
accumulateSinglePhaseListeners
accumulateSinglePhaseListeners函数里就是在向上层遍历来收集一个列表后面会用来模拟冒泡。最后的数据结构如下:
dispatchQueue的数据结构为数组,类型为[{ event,listeners }]。这个
listeners则为一层一层收集到的数据,类型为[{ currentTarget, instance, listener }]processDispatchQueue
processDispatchQueue函数里会遍历dispatchQueue。dispatchQueue中的每一项在processDispatchQueueItemsInOrder函数里遍历执行。processDispatchQueueItemsInOrder
processDispatchQueueItemsInOrder函数里会根据判断来正向、反向的遍历来模拟冒泡和捕获。executeDispatch
executeDispatch函数里会执行listener。结语
本文旨在理清事件机制的执行,按照函数执行栈简单的罗列了代码逻辑,如果不对照代码看是很难看明白的,原理在开篇就讲述了。
React的事件机制隐晦而复杂,根据不同情况做了非常多的判断,并且还有优先级相关代码、合成事件,这里都没有一一讲解,原因当然是我还没看~
平时用React也就写写简单的手机页面,以前老板还经常吐槽加载不够快,那也没啥办法,就对我的工作而言,有没有
Cocurrent都是无关紧要的,这合成事件更复杂,完全就是不需要的,不过React的作者们脑洞还是牛皮,要是没看源码我肯定是想不到竟然模拟了一套事件机制。小思考
stopPropagation可以阻止合成事件的传递?这些问题我放以前根本没想过,不过今天看了源码以后才想的。
因为合成事件是在原生事件触发之后才开始收集并触发的,所以当原生事件调用
stopPropagation阻止传递后,根本到不到root节点,触发不了React绑定的处理函数,自然合成事件也不会触发,所以原生事件不是阻止了合成事件的传递,而是阻止了React中绑定的事件函数的执行。比如这个例子,在原生onClick阻止传递后,控制台连“合成事件”这4个字都不会打出来了。
Beta Was this translation helpful? Give feedback.
All reactions