每日一博 | React 事件系统是如何工作的?
一、DOM 事件流
在浏览器中,我们通过事件监听来实现 JS 和 HTML 之间的交互。一个页面往往会被绑定许许多多的事件,而页面接收事件的顺序,就是事件流。它类似于蹦床,从高处下落,触达蹦床后再弹起,整个过程呈一个V字形。若按W3C标准,一个事件的传播过程要经过三个阶段
1、DOM 事件流的三个阶段
-
事件捕获阶段 事件从最外层的元素开始“穿梭”,逐层“穿梭”,直到目标元素,也就是真正触发事件的元素
-
目标阶段 事件被目标元素所接收
-
事件冒泡阶段 事件被“回弹”,沿着来时的路“逆流而上”,逐层往上
2、事件委托
假设我们有这么一个场景:在拥有1000个li
元素的列表上,点击每一个li
输出其对应的文本内容
很直观的一个思路:让每个li
元素去监听一个点击动作,但这样重复的代码不够优雅,开销也蛮大。若利用 DOM 事件流的事件冒泡特性,我们可以这么做:把多个子元素的同一类型的监听逻辑合并到父元素上,通过一个监听函数来管理行为,即通过事件对象中的target
属性,获取到真正触发事件的元素,这也是所谓的事件委托
let ul = document.getElementsByTagName('ul') ul.addEventListener('click', function(e){ // e.target属性指的是触发事件的具体目标,记录着事件的源头 console.log(e.target.innerHTML) })
通过事件委托处理,可减少内存开销、简化注册步骤,从而提高开发效率。这也给了react 16
灵感,实现对所有的事件的中心化管理。
二、React 事件系统
当事件在具体的DOM
节点上被触发后,最终都会冒泡到document
上(除了少数特殊的不可冒泡的事件,例如媒体类型的事件外),document
上所绑定的统一事件处理程序会将事件分发到具体的组件实例
在分发事件之前,React
首先会对事件进行包装,把原生DOM
事件包装成合成事件
1、React合成事件
在React 16
及之前版本中,React
自定义的合成事件主要在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的,稳定的,与DOM
原生事件相同的事件接口,同时它保存了原生DOM
事件的引用。当开发者需要访问原生DOM
事件对象时,可通过合成事件对象的e.nativeEvent
属性获取到它
2、React事件系统的工作流
说到事件系统,就有事件的绑定和触发两个关键动作,其中事件的绑定是在挂载阶段里的completeWork
函数完成的。completeWork
函数内部做了三个关键动作:
- 创建
DOM
节点 - 将
DOM
节点插入到DOM
树中 - 为
DOM
节点设置属性 - 该环节会遍历FiberNode
的props key
,当遍历到事件相关的props
时,便会触发事件的注册链路
事件绑定
在react16
源码的基础上,我们来看看事件的注册过程:
其中,源码中有一段判断逻辑值得我们关注
// listenerMap: 记录当前document已经监听了哪些事件 // topLevelType: 事件的类型 listenerMap.has(topLevelType)
若事件系统识别到 listenerMap.has(topLevelType)
为 true
,则说明该函数 document
已经监听过了,直接跳过。因此,即便我们在 react
项目中多次调用对同一个事件的监听,也只会在 document
上触发一次注册。
Q: 为什么针对同一个事件,即便可能会存在多个回调,document
也只需注册一次监听?
A:react 最终注册到 document
上的并不是某一个DOM
节点上对应的具体回调逻辑,而是一个统一的事件分发函数dispatchEvent
事件触发
同样,在react16
源码的基础上,我们来看看事件的触发过程:
其中,事件回调的收集与执行值得我们关注,它主要做了以下三件事:
- 循环收集符合条件(DOM元素对应的Fiber节点)的父节点,存进path数组
- 模拟事件在捕获阶段的传播顺序,收集捕获阶段相关节点对应的回调与实例
- 模拟事件在冒泡阶段的传播顺序,收集冒泡阶段相关节点对应的回调与实例
接下来,我们来看看源码是如何巧妙的模拟出完整的DOM 事件流
function traverseTwoPhase(inst, fn, arg) { // 定义一个 path 数组:子节点在前,祖先节点在后 var path = []; while (inst) { // 将当前节点收集进 path 数组 path.push(inst); // 向上收集 tag===HostComponent 的父节点 inst = getParent(inst); } var i; // 模拟捕获阶段:从后往前,收集 path 数组中会参与捕获过程的节点与对应回调 for (i = path.length; i-- > 0;) { // fn 函数对节点进行检查,若回调不为空,则将实例收集到 SyntheticEvent._dispatchInstances,事件回调则被收集到 SyntheticEvent._dispatchListeners fn(path[i], 'captured', arg); } // 模拟冒泡阶段:从前往后,收集 path 数组中会参与冒泡过程的节点与对应回调 for (i = 0; i < path.length; i++) { // 同上 fn(path[i], 'bubbled', arg); } }
traverseTwoPhase
函数主要做了三件事:
重点强调的是:当前事件对应的合成事件实例有且只有一个,因此在模拟捕获和冒泡两个过程,收集到的实例会被存入同一个SyntheticEvent._dispatchInstances
,同样,收集到的事件回调也会被存入同一个SyntheticEvent._dispatchListeners
。因此,只需要按顺序执行SyntheticEvent._dispatchListeners
数组中的回调函数,就能模拟出完整的DOM 事件流,即“捕获-目标-冒泡”三个阶段
三、React16
事件系统的设计动机是什么?
1、React 官方说明过的一点是:合成事件符合W3C规范,在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口。开发者们由此便不必再关注烦琐的底层兼容问题,可以专注于业务逻辑的开发
2、自研事件系统使 React 牢牢把握住了事件处理的主动权,能够从很大程度上干预事件的表现,使其符合自身的需求,毕竟原生讲究的就是个通用性。而 React 想要的则是“量体裁衣”。
四、React16
事件系统的不足
在GitHub issue
里有一个这样的Bug
:
提问者试图在input
元素的React
事件函数中阻止冒泡,但事实并没有如愿,每次点击input
的时候,事件还是会被冒泡到document
上去。对此,他得到的回复是这样的:
React
会通过将所有事件冒泡到document
来实现对事件的中心化管控,而document
是整个文档树的根节点,操作它带来的影响范围着实太大。
提问者在handleClick
这个React
事件函数中阻止了冒泡,但这只能保证该事件对应的合成事件在React
事件体系下的冒泡被阻止了,即React
不会为这个合成事件 模拟冒泡效果,并不能阻止原生DOM
事件的冒泡,因此安装在document
上的事件监听器一定会被触发
且不说document
中心化管控这个设定给开发者带来了多大的限制,单看document
是一个全局的概念,而组件只是全局的一个部分,便能多少预感到其中的风险。
五、React17
的改进
1、放弃利用document
来做事件的中心化管控
React 17
正面解决了这个问题:事件的中心化管控不会再全部依赖document
,管控相关的逻辑被转移到了每个React
组件自己的容器DOM
节点上。
在 React 16
及之前版本中,React
会对大多数事件进行 document.addEventListener()
操作。React 17
开始会通过调用 rootNode.addEventListener()
来代替。
这样一来,React
组件就能够各玩各的,再也无法对全局的事件流造成影响
2、放弃事件池
在React 17
之前,合成事件对象会被放进一个叫作:“事件池”的地方统一管理。其目的是为了实现事件的复用,进而提高性能。即当所有事件处理函数被调用之后,其所有属性都会被置空,换句话说:事件逻辑一旦执行完毕,开发者就拿不到事件对象了
有个官方的例子 如下:
function handleChange(e) { // This won't work because the event object gets reused. setTimeout(() => { console.log(e.target.value); // Too late! }, 100); }
异步执行的setTimeout
回调会在handleChange
这个事件处理函数执行完毕后执行,因此它拿不到想要的那个事件对象,如果你需要在事件处理函数运行之后获取事件对象的属性,你需要调用 e.persist()

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Inkscape 1.1 发布,超强的跨平台矢量图形编辑软件
Inkscape 1.1现已发布。Inkscape 是一个矢量图形编辑软件,与 Illustrator、Freehand、CorelDraw、Xara X 等软件很相似,它使用 W3C 标准的 Scalable Vector Graphics (SVG) 文件格式,支持包括形状、路径、文本、标记、克隆、alpha 混合、变换、渐变、图案、组合等 SVG 特性。它也支持创作共用的元数据、节点编辑、图层、复杂的路径运算、位图描摹、文本绕路径、流动文本、直接编辑 XML 等。它可以导入 JPEG、PNG、TIFF 等格式,并输出为 PNG 和多种矢量格式。 全新的欢迎界面 在启动 Inkscape 1.1 时将出现一个全新的欢迎界面,欢迎对话框允许用户通过选择画布颜色、键盘快捷键样式、主题集和颜色模式来自定义体验。 新的命令调色板 新的命令调色板可以通过触摸 ? 键打开,使用户能够搜索和使用各种功能,而不必求助于菜单或快捷键。用户可以使用编辑、旋转和重置,以及其他已经被转换为 " actions" 的命令,也可以从 Inkscape 的文档历史中导入或打开文件。 更高级别的对接体验 改进后...
- 下一篇
KubeOperator —— 开源轻量级 Kubernetes 发行版
KubeOperator 是一个开源的轻量级 Kubernetes 发行版,专注于帮助企业规划、部署和运营生产级别的 Kubernetes 集群。 KubeOperator 提供可视化的 Web UI,支持离线环境,支持物理机、VMware 和 OpenStack 等 IaaS 平台,支持 x86_64 和 arm64 架构,支持 GPU,内置应用商店,已通过 CNCF 的 Kubernetes 软件一致性认证。 KubeOperator 使用 Terraform 在 IaaS 平台上自动创建主机(用户也可以自行准备主机,比如物理机或者虚机),通过 Ansible 完成自动化部署和变更操作,支持 Kubernetes 集群 从 Day 0 规划,到 Day 1 部署,到 Day 2 运营的全生命周期管理。 整体架构 Web UI 展示 更多功能截屏点击:这里 技术优势 简单易用:提供可视化的 Web UI,极大降低 K8s 部署和管理门槛,内置Webkubectl; 按需创建:调用云平台 API,一键快速创建和部署 Kubernetes 集群; 按需伸缩:快速伸缩 Kubernetes...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker使用Oracle官方镜像安装(12C,18C,19C)