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元素上监听所有支持的事件。
listentoallsupportedevents
在绑定事件时,会通过名为allnativeevents的set变量来获取对应的eventname,这个变量会在一个顶层函数进行收集,而nondelegatedevents是一个预先定义好的set。
listentonativeevent
listentonativeevent函数在绑定事件之前会先将事件名在dom元素中标记,判断为false时才会绑定。
addtrappedeventlistener
addtrappedeventlistener函数会通过事件名取得对应优先级的listener函数,在交由下层函数处理事件绑定。
这个listener函数是一个闭包函数,函数内能访问targetcontainer、domeventname、eventsystemflags这三个变量。
addeventcapturelistener函数和addeventbubblelistener函数内部就是调用原生的target.addeventlistener来绑定事件了。
这一步是循环一个存有事件名的set,将每一个事件对应的处理函数绑定到root节点dom元素上。
不需要委托事件绑定
不需要委托的事件其中也包括媒体元素的事件。
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个字都不会打出来了。
以上就是react事件机制源码解析的详细内容,更多关于react事件机制源码的资料请关注其它相关文章!