面试官:请说一下React事件原理

最近在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函数,只不过根据情况的不同,传参不一样而已。

这里面,我们来看一下allNativeEventsnonDelegatedEvents分别代表什么含义:

allNativeEvents代表着浏览器支持的所有交互事件。

nonDelegatedEvents代表着只支持在捕获阶段被触发的事件。也可以说成是 不支持事件委托的事件集合。

我们再接着往下看 listenToNativeEvent函数:

这个函数里又调用了addTrappedEventListener,除此之外还进行了eventSystemFlags的逻辑运算。

这个逻辑运算的意义就是,如果事件是在捕获阶段触发,eventSystemFlags就一定大于0,否则冒泡阶段就一定是0。

我们接着看一下 addTrappedEventListener 的执行逻辑:

这个函数2个执行逻辑,一个是createEventListenerWrapperWithPriorityaddEventXXListener

我们来看一下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了。读过我的文章的小伙伴应该都知道,每篇文章我都会控制字数,因为在我看来,如果一篇文章的字数过长,会给人一种臃肿的感觉,大概率别人看了也不知道作者在说啥,只会感觉到累。

如果上述过程中有明显的错误,欢迎小伙伴们评论区里指正,那么我们下期再见啦,拜拜~~

相关推荐
道不尽世间的沧桑27 分钟前
第9篇:插槽(Slots)的使用
前端·javascript·vue.js
bin915331 分钟前
DeepSeek 助力 Vue 开发:打造丝滑的滑块(Slider)
前端·javascript·vue.js·前端框架·ecmascript·deepseek
uhakadotcom1 小时前
最新发布的Tailwind CSS v4.0提供了什么新能力?
前端
GISer_Jing1 小时前
Node.js中如何修改全局变量的几种方式
前端·javascript·node.js
秋意钟1 小时前
Element UI日期选择器默认显示1970年解决方案
前端·javascript·vue.js·elementui
bramble1 小时前
Windows使用Trae全程提问来创建一个彩色贪吃蛇游戏,可以设置速度并查看游戏记录。
前端·程序员·trae
我命由我123452 小时前
微信小程序 - 自定义实现分页功能
前端·微信小程序·小程序·前端框架·html·html5·js
程序员黄同学2 小时前
请谈谈 Vue 中的 key 属性的重要性,如何确保列表项的唯一标识?
前端·javascript·vue.js
繁依Fanyi3 小时前
巧妙实现右键菜单功能,提升用户操作体验
开发语言·前端·javascript·vue.js·uni-app·harmonyos
前端御书房3 小时前
前端防重复请求终极方案:从Loading地狱到精准拦截的架构升级
前端·javascript