2021-05-26

本文深入探讨了React事件系统的运作机制,包括DOM事件流、事件委托、React合成事件以及事件分发。重点阐述了React如何通过事件委托优化性能,以及在React16中对事件的中心化管理和事件池的设计。此外,还提到了React17对事件系统的改进,如放弃document的中心化管控和废弃事件池,以提高组件间的独立性和性能。

  React 事件系统是如何工作的?

  一、DOM 事件流

  在浏览器中,我们通过事件监听来实现 JS 和 HTML 之间的交互。一个页面往往会被绑定许许多多的事件,而页面接收事件的顺序,就是事件流。它类似于蹦床,从高处下落,触达蹦床后再弹起,整个过程呈一个V字形。若按W3C标准,一个事件的传播过程要经过三个阶段

  1、DOM 事件流的三个阶段

  事件捕获阶段 事件从最外层的元素开始“穿梭”,逐层“穿梭”,直到目标元素,也就是真正触发事件的元素

  目标阶段 事件被目标元素所接收

  事件冒泡阶段 事件被“回弹”,沿着来时的路“逆流而上”,逐层往上

  

11f2a4f02636e6d6undefined

 

  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 16 及之前版本中的事件系统

 

  在分发事件之前,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() 来代替。

  

放弃利用document来做事件的中心化管控

 

  这样一来,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()

内容概要:本文详细介绍了一个基于C++的养老院管理系统的设计与实现,旨在应对人口老龄化带来的管理挑战。系统通过整合住户档案、健康监测、护理计划、任务调度等核心功能,构建了从数据采集、清洗、AI风险预测到服务调度与可视化的完整技术架构。采用C++高性能服务端结合消息队列、规则引擎和机器学习模型,实现了健康状态实时监控、智能任务分配、异常告警推送等功能,并解决了多源数据整合、权限安全、老旧硬件兼容等实际问题。系统支持模块化扩展与流程自定义,提升了养老服务效率、医护协同水平和住户安全保障,同时为运营决策提供数据支持。文中还提供了关键模块的代码示例,如健康指数算法、任务调度器和日志记录组件。; 适合人群:具备C++编程基础,从事软件开发或系统设计工作1-3年的研发人员,尤其是关注智慧养老、医疗信息系统开发的技术人员。; 使用场景及目标:①学习如何在真实项目中应用C++构建高性能、可扩展的管理系统;②掌握多源数据整合、实时健康监控、任务调度与权限控制等复杂业务的技术实现方案;③了解AI模型在养老场景中的落地方式及系统架构设计思路。; 阅读建议:此资源不仅包含系统架构与模型描述,还附有核心代码片段,建议结合整体设计逻辑深入理解各模块之间的协同机制,并可通过重构或扩展代码来加深对系统工程实践的掌握。
内容概要:本文详细介绍了一个基于C++的城市交通流量数据可视化分析系统的设计与实现。系统涵盖数据采集与预处理、存储与管理、分析建模、可视化展示、系统集成扩展以及数据安全与隐私保护六大核心模块。通过多源异构数据融合、高效存储检索、实时处理分析、高交互性可视化界面及模块化架构设计,实现了对城市交通流量的实时监控、历史趋势分析与智能决策支持。文中还提供了关键模块的C++代码示例,如数据采集、清洗、CSV读写、流量统计、异常检测及基于SFML的柱状图绘制,增强了系统的可实现性与实用性。; 适合人群:具备C++编程基础,熟悉数据结构与算法,有一定项目开发经验的高校学生、研究人员及从事智能交通系统开发的工程师;适合对大数据处理、可视化技术和智慧城市应用感兴趣的技术人员。; 使用场景及目标:①应用于城市交通管理部门,实现交通流量实时监测与拥堵预警;②为市民出行提供路径优化建议;③支持交通政策制定与信号灯配时优化;④作为智慧城市建设中的智能交通子系统,实现与其他城市系统的数据协同。; 阅读建议:建议结合文中代码示例搭建开发环境进行实践,重点关注多线程数据采集、异常检测算法与可视化实现细节;可进一步扩展机器学习模型用于流量预测,并集成真实交通数据源进行系统验证。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值