最近在React源码上颇有用功,所以决定将我的思想感悟记录于此,也欢迎大家及时纠错。
如果在面试中,面试官问了你这样的问题:请说一下React事件机制
。我觉着你可以从下面的思维导图去入手:
那我们就顺着这个思维导图来一点点讲解吧。
浏览器原生事件
正式开讲之前,还是有必要来回顾一下浏览器事件原理的。
浏览器提供了非常多的事件,我们可以通过绑定函数的方式来处理不同的事件。绑定函数的过程可以叫做"注册事件处理器",也可以叫做"注册事件监听器"。
按照事件处理器被触发的顺序不同,我们又分为了捕获阶段、冒泡阶段。如果你要想让自己的事件在捕获阶段被触发,那你就要遵守相应的规则,如果你要想让自己的事件在冒泡阶段被触发,也要遵守相应的规则。
有的事件只能在捕获阶段被触发,有的事件只能在冒泡阶段被触发。有的事件既可以支持捕获阶段触发,也支持冒泡阶段触发。
所以,当你注册事件处理器的时候,你必须要知道自己的事件支持哪种触发方式。否则很容易就会出现自己看不懂的情况。
讲完了基本理论,我们再来看看注册方式之间的差异:
是否支持重复注册 | 灵活性 | |
---|---|---|
onxxx | 支持。但是后者会覆盖前者,执行的永远都是后者。 | 弱 |
addEventListener | 支持。后者不会覆盖前者,先注册的先执行。 | 强。第三个参数让开发者有更多的支配权 |
什么是合成事件?
定义
它是一套封装了原生浏览器事件的跨浏览器事件系统。
存在的意义
其实它最主要的意义就在于它解决了不同事件在不同浏览器上的差异问题。
工作流程
其实每次我写React相关的东西的时候,我都比较痛苦。为啥呢,它的关联性太强了,你很难找到一个切面去把它完全讲解透。
注册
我们以下面这段代码为例,然后尽量看个透(本篇文章里的React版本是18)。
jsx
import React from 'react';
function PageOne(){
return <div>
<button className='button-1' onClick={clickAddButtonEvent}>+1</button>
</div>
}
export default PageOne;
当这个组件初次被mount的时候,大致走了以下源码:
其中,我们只关注红框里的内容即可。红框外的内容主要就是创建fiber、生成dom。
我们再调试一下listenToAllSupportedEvents
函数:
从上图可以看到,它就是调用了listenToNativeEvent
函数,只不过根据情况的不同,传参不一样而已。
这里面,我们来看一下allNativeEvents
、nonDelegatedEvents
分别代表什么含义:
allNativeEvents代表着浏览器支持的所有交互事件。
nonDelegatedEvents代表着只支持在捕获阶段被触发的事件。也可以说成是 不支持事件委托的事件集合。
我们再接着往下看 listenToNativeEvent
函数:
这个函数里又调用了addTrappedEventListener
,除此之外还进行了eventSystemFlags
的逻辑运算。
这个逻辑运算的意义就是,如果事件是在捕获阶段触发,eventSystemFlags就一定大于0,否则冒泡阶段就一定是0。
我们接着看一下 addTrappedEventListener
的执行逻辑:
这个函数2个执行逻辑,一个是createEventListenerWrapperWithPriority
、addEventXXListener
。
我们来看一下createEventListenerWrapperWithPriority
的执行逻辑:
这里也比较明显,主要就是将浏览器事件进行分类,分为不同的优先级,每类优先级返回不同的函数dispatchXXEvent
。
拿到 listener 后,执行addEventXXListener
来给id为root的dom节点进行事件绑定。
至此,React框架里,事件的注册流程就结束了。下面是整个注册过程的流程图。
执行
从上面的分析我们会发现2点信息:
- 我们所有的事件都绑定在了id为root的dom节点身上。
- React为不同优先级的事件类型创建了不同的事件函数。
从上图我们可以发现,无论哪种优先级类型的事件绑定函数,最终都会执行dispatchEvent
函数。
我们来看一下dispatchEvent
函数的执行逻辑:
如果我们在下面地方打断点:
我们会发现一个现象,似乎只要你的鼠标悬浮上去,这个断点就会被卡住。此时domEventName的值要么是"pointermove",要么是"pointerout"。所以我们需要在这个断点处添加判断,只有domEventName等于"click"的时候再执行。
结果不出所料,当我们点击button按钮进行+1操作时,确实是在这个断点处停住了,所以我们现在的当务之急就是看看findInstanceBlockingEvent(寻找阻塞事件的实例)
函数都干了什么事。
1、首先,它通过事件委托,找到了你当前点击时所对应的元素,并将结果赋值给了targetInst。
2、进行判断,如果targetInst是原生标签,那就直接返回null。如果当前组件是Suspense
组件,那就返回它的实例。如果当前组件是根节点(root),那就直接返回这个根节点就可以了。
所以当你再回顾dispatchEvent
函数的时候,你会发现,它其实就是针对不同的组件类型做不同的处理,但是最终操作还是要落实到dispatchEventForPluginEventSystem
函数。
所以我们再来看一下这个函数的执行流程(这里篇幅有限,仅展示部分代码):
执行批量更新(batchedUpdates),我们接着看一下dispatchEventsForPlugins函数
。
extractEvent$5负责收集组件上的on + 原生事件
属性,并将它放入到dispatchQueue,然后processDispatchQueue负责执行这个队列里的事件函数。
到这里,React框架里的合成事件的触发流程也就结束啦,我们来贴一张图领会下刚才的流程。
执行顺序
这个要看原生事件的触发时机是冒泡阶段还是捕获阶段。
如果这个原生事件A
的默认触发时机是冒泡阶段,那么当一个组件既绑定了合成事件,又绑定了原生事件,此时原生事件的执行顺序是一定在合成事件前面的。因为这个事件A一个真实的绑定在了root上,一个真实的绑定在了dom上,而且触发时机也是冒泡阶段触发。
如果这个原生事件B
的默认触发时机是捕获阶段,那就恰好相反了,合成事件的执行时机一定比原生事件早。
最后
2023年-12-10号,凌晨1:50,终于写完了。因为React框架的代码比较分散,所以上述过程中有一些重要的代码没有讲,比如说React是如何收集合成事件的等等(只是一句话带过了),欢迎小伙伴在评论区里补充,我就不在这篇文章里写了,这正文字数都已经1832了。读过我的文章的小伙伴应该都知道,每篇文章我都会控制字数,因为在我看来,如果一篇文章的字数过长,会给人一种臃肿的感觉,大概率别人看了也不知道作者在说啥,只会感觉到累。
如果上述过程中有明显的错误,欢迎小伙伴们评论区里指正,那么我们下期再见啦,拜拜~~