React作为目前前端业界最流行的mvvm框架之一已经被广大前端同学所熟知,而在日常工作中已经熟悉使用React的我们对React内部的工作流程、设计理念是否又有足够的了解呢?本文是对于React事件(DOM)系统的研究。

●●● 

React事件系统概况

1.1React的事件处理

熟练使用React的前端同学相信对于以下这段代码并会不陌生:

上面这张图是一个从React官网借来的事件处理的例子,在React中想要让某些元素监听事件的方法就是在元素上(在这里是<a>)设置想要监听的事件(onClick)以及事件触发后的回调函数(handleClick), 而真的事件一旦发生后React是如何处理并触发事件回调的呢,我们今天就要研究其内部的工作机制。

1.2 合成事件

在上面那张图里handleClick函数的参数e并不是我们通常以为的DOM原生事件,而是React事件系统独有的概念:合成事件SyntheticEvent,合成事件是React对原生事件(DOM)的抽象封装,合成事件的属性包括

  • nativeEvent 合成事件对应的原生事件

  • preventDefault() 阻止默认行为

  • stopPropagation() 阻止事件传播

  • currentTarget 当前事件处理的元素

  • target 触发事件的元素

  • 等其他属性和方法

而React之所以要创造合成事件这个新的概念,其主要原因是要抹平跨浏览器差异,因为不同的浏览器的事件机制处理不尽相同,所以React需要这一层额外的封装来统一React的事件处理行为;此外,React的事件系统还使用了事件池来存放事件以提高性能,减少内存开销。

1.3 React事件系统总览

我们先来看看React官方对于事件系统工作流程的描述,下图来自官方源码: https://github.com/facebook/react/blob/master/packages/react-dom/src/events/ReactBrowserEventEmitter.js

通过上图我们大致可以知道,React事件系统由几部分构成:

  • ReactEventListener负责监听DOM事件

  • ReactEventEmitter负责将事件传送给EventPluginHub

  • EventPluginHub(事件插件容器的容器,这个容器负责容纳各种所谓的事件插件(EventPlugin)

  • EventPlugin的工作就是将原生事件转换为React自定义的合成事件(SyntheticEvent),并搜集这个事件都有哪些元素在监听和监听的回调,最后在应用层级(application)触发回调。

现在我们对React事件系统已经有了一个抽象的了解,下面我们具体看看React是如何完成从绑定监听、事件触发到调用回调这样一个完整的流程的。

●●● 

React事件系统工作机制

2.1 React源码事件系统相关代码介绍

在进入介绍React事件系统工作机制之前,我们先了解一下React源码中哪些部分在此次的讨论范围之内(以2018-04-13 v16.3版本为准)

  • packages/events是事件系统的核心代码,内含合成事件与EventPlugin相关模块,是跨平台(DOM,ReactNative)通用的抽象事件处理相关代码

  • packages/react-dom/src/client 包含React Virtual DOM相关的核心代码

  • packages/react-dom/src/events 包含与DOM相关的事件处理代码,包括DOM的事件监听器ReactDOMEventListener等等。

下面来介绍React事件系统的具体工作流程

2.2 绑定监听阶段

在早先的前端开发年代,我们想要监听某个元素上的事件通常会直接给这个元素加上事件监听(或者通过给其母元素加上事件监听代理),但无论如何事件监听都设置在了该元素或者该元素相近元素的身上,React监听DOM事件的逻辑则不同,React会在Document层面统一监听事件,而不会在具体元素上添加eventListener,这样避免了在UI复杂的情况下要给数量众多的DOM元素增加eventListener的过程,React统一在Document层面监听事件并寻找各个监听了该事件的Component,再执行它们的回调。而React绑定事件监听的流程如下

(1).在React准备渲染阶段,ReactDOMFiberComponent.js(Virtual DOM的最小独立单元Fiber)会调用setInitialDOMProperties方法初始化React元素的属性

(2).setInitialDOMProperties方法里如果发现这个组件设置了监听事件就会调用ensureListeningTo方法来监听这个事件(具体的监听绑定不在此处)

(3).ensureListeningTo会调用ReactBrowserEventEmitter模块的listenTo方法来进行对具体事件的监听,listenTo方法会根据事件类型来决定监听是在捕获还是冒泡阶段。(scroll,focus,blur,cancel,close事件监听捕获阶段,其他时间监听在冒泡阶段)

(4).listenTo方法调用ReactDOMEventListener模块的trapCapturedEvent/trapBubbledEvent方法进行监听

(5).trapCapturedEvent/trapBubbledEvent会调用EventListener.js的addEventBubbleListener/addEventCaptureListener方法进行DOM层级的事件监听,而监听的回调是一个叫做dispatchEvent的方法,dispatchEvent是DOM原生事件触发后React事件处理流程的起点.

至此,React时间系统的事件监听绑定完成.

2.3 事件触发至生成合成事件阶段

当DOM的原生事件触发后,React事件系统会如何运转呢,上一部分提到ReactDOMEventListener的dispatchEvent方法会被触发,以下是事件触发后的流程

(1).原生事件被触发:比如,你点击了一个button

(2).ReactDOMEventListener的dispatchEvent方法会收到原生事件,并将原生事件和将触发事件元素所在的虚拟DOM节点等其他信息组合成一个叫Bookkeeping的对象

(3).ReactDOMEventListener会调用handleTopLevel方法来处理Bookkeeping对象,在handleTop方法内调用EventPluginHub.js的runExtractedEventsInBatch方法,此时事件处理的任务让渡给EventPluginHub(如1.3图中所示)

(4).EventPluginHub的runExtractedEventsInBatch方法会调用extractEvents方法,extractEvents函数的作用就是根据原生事件等信息生成相应的合成事件

(5).extractEvents函数的执行过程中会遍历所有该原生事件相关的事件插件(EventPlugin),让每一个EventPlugin生成对应的合成事件,并将所有合成事件放在一个数组里

(6).EventPlugin在生成合成事件的过程中收集所有用户编写的事件监听回调(比如1中的handleClick)

这一阶段完成了从原生事件触发到生成合成事件(收集需要执行的回调函数)的工作.

2.4 执行回调阶段

EventPluginHub在获得了某一原生事件相关的一个或多个合成事件后,开始执行这些事件的回调,其具体流程如下

(1).EventPluginHub对合成事件数组执行runEventsInBatch方法,该方法的作用是处理合成事件数组

(2).runEventsInBatch会对合成事件队列里的每一个合成事件执行executeDispatchesAndReleaseTopLevel,这个方法会先调用EventPluginUtils模块的executeDispatchesInOrder,并且在之后将合成事件释放(除非用户手动选择将该合成事件持久化)

(3).EventPluginUtils的executeDispatchesInOrder会找到所有监听这个事件的React元素(FiberNode)和回调(比如handleClick),然后执行每一个回调.

至此React事件系统完整的处理了一个事件监听,用户编写的事件监听回调执行完毕.

重新快速梳理一遍React事件系统的工作流程就是

  • ReactDOMFiberComponent模块初始化Virtual DOM节点的属性

  • ReactBrowserEventEmitter/ReactDOMEventListener/EventListener模块会对用户设置的事件进行监听

  • 原生事件触发

  • ReactDOMEventListener将原生事件交给EventPluginHub处理

  • EventPluginHub让各个相关的EventPlugin生成合成事件

  • EventPluginHub让合成事件队列里的各个合成事件执行相应的回调

●●● 

React事件系统思考题

3.1 React事件和原生事件回调的执行顺序

Q: 如果给一个button设置了onClick回调handleClick1,又手动给这个button的点击事件手动设置addEventListenerhandleClick2,如果点击这个按钮,哪个回调先执行?

A: handleClick2先执行,handleClick1再执行,因为React在Document层级冒泡阶段监听click事件,所以当原生事件在button上先触发handleClick2上再冒泡到document上,然后React再启动自有的事件处理流程.

追加问题:如果是blur,focus等不冒泡的事件,比如给input设置onBlur,又给input手动添加blur的eventListener,当事件触发时哪个先执行?

3.2 阻止冒泡的处理

Q: 如图有如下组件, onBtnClick里调用了合成事件的阻止冒泡方法stopPropagation,那么图中button的母元素div绑定的两个事件回调onDivNativeClick和onDivClick是否会执行?

想知道答案的同学可以去https://codesandbox.io/s/1q8n95oovq试试

3.3 React在Document层级监听事件,如果有多个元素(不在同一条子树上)绑定同一事件,React如何准确触发回调?

在2(6)步骤中,会调用accumulateTwoPhaseDispatches函数,这个函数会模拟事件的捕获/冒泡流程,收集virtual dom中事件触发的元素到根元素之间路径上每个元素针对这个事件的回调,只有在这里收集到的回调才会稍后被执行,所以不存在会误触发其他监听同样事件元素的回调.

以上是此次React事件系统的研究,为了研究React内部实现的工程细节所以讨论的粒度较细,谢谢阅读!

参考资料

  • React官网: 事件处理     https://reactjs.org/docs/handling-events.html

  • React官网: 合成事件 https://reactjs.org/docs/events.html

  • React源码: https://github.com/facebook/react

  • https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2

  • https://www.kirupa.com/react/events_in_react.htm

——推荐阅读——

网易云信IM小程序上线?我们是这么做的!>>

全面复盘!深度剖析直播答题产品架构的难点与坑>>

如何快速设计短信验证码>>

如何做好Android 端音视频测试>>

React事件系统研究总结相关推荐

  1. React 事件 4

    微信小程序开发交流qq群   173683895    承接微信小程序开发.扫码加微信. 将参数传递给事件处理程序 在循环内部,通常需要将额外的参数传递给事件处理程序.例如,如果id是行ID,则以下任 ...

  2. React事件机制 - 源码概览(下)

    上篇文档 React事件机制 - 源码概览(上)说到了事件执行阶段的构造合成事件部分,本文接着继续往下分析 批处理合成事件 入口是 runEventsInBatch // runEventsInBat ...

  3. React事件的问题

    React 把事件委托到 document 对象上. 当真实 DOM 元素触发事件,先处理原生事件,然后会冒泡到 document 对象后,再处理 React 事件. React事件绑定的时刻是在 r ...

  4. 彻底搞懂原生事件流和 React 事件流

    事件系统,业务开发中只要需要与用户进行交互,那么事件是必不可少的,dom 中存在很多事件,比如 click,scroll,focus 等等.我们将深入事件系统中,以及事件中常用的一些操作比如 prev ...

  5. React 事件onClick

    React 事件处理(冒泡及阻止默认事件) react 事件注意的点以及案例详情 1.驼峰形式 onClick 2.调用 onClick={this.Fn}this指向问题 3.解决指向1.箭头函数 ...

  6. React事件优雅绑定

    2018-01-11 19:00 更新(目前最优解) 收到 光 同学的提醒,与其让ESLint报错,不如在调用时候做一些修改,上代码,就是在调用事件的时候,重新用箭头函数再包裹一层,拍脑袋表示为啥自己 ...

  7. React 事件总结

    目录 一 绑定事件处理函数 1.1 鼠标类 1.2 拖拽事件: 1.3 触摸 1.4 键盘 1.5 剪切类 1.6 表单类 1.7 焦点事件 1.8 UI元素类 1.9 滚动 1.10 组成事件 1. ...

  8. react事件 组件设计传参使用

    1.事件 和原生js事件一致 事件命名用驼峰式 onclick onClick onmouseover onMouseover 事件总是要响应一个函数 render() {return ( <d ...

  9. 04.React事件 方法、 React定义方法的几种方式 获取数据 改变数据 执行方法传值...

    2019独角兽企业重金招聘Python工程师标准>>> 一.基本用法 在以类继承的方式定义的组件中,为了能方便地调用当前组件的其他成员方法或属性(如:this.state),通常需要 ...

最新文章

  1. Android4.4 ContentResolver查询图片无效 及 图库删除 添加图片后,ContentResolver不更新的问题解决...
  2. 修正CS2.0中的日历头显示错误
  3. PostgreSQL 8.4.3 Final / 9.0 Alpha 4
  4. sql 数字转换为16进制数函数
  5. 大话设计模式-策略模式与简单工厂模式
  6. C# web项目中sql数据库转sqlite数据库
  7. 有时间了要研究一下Stack Exchange的开源项目
  8. backup ram不稳定 stm32_具备无线能力的STM32,如何让智能手表更加炙手可热?
  9. java jpanel 叠加_java – 如何在JPanel上叠加,调整大小和居中组件?
  10. nvidia-smi 显示无进程,但GPU显存被占用
  11. EViews简介、下载安装教程
  12. 【数字信号处理课程设计】基于MATLAB实现语音信号的采集与处理(偏重滤波)
  13. 常见气象数据获取方式及批量下载代码汇总
  14. 二维码的应用领域有哪些?
  15. JavaScript 实现全选,分组全选,列表折叠。
  16. R语言处理非线性回归模型C-D方程,【译文】R语言非线性回归入门
  17. HTML重构:工具篇
  18. ADAS——高级驾驶辅助系统介绍
  19. JSON的入门介绍学习
  20. 数据库系统概论整理(Part Ⅰ)

热门文章

  1. 计算数组的逆序对个数
  2. 2021-11-14Iterator迭代器
  3. FEKO V7.0安装教程
  4. Keil forc51安装教程
  5. 小鸡模拟器 android,安卓小鸡模拟器运行总结~
  6. 并发量与RAID_RAID 技术全解 – RAID0、RAID1、RAID5、RAID10-宿主机磁盘阵列-香港母机...
  7. 学习SOX(1) 在VC中编译
  8. linux进去网卡,Linux上使用socket进行网卡抓包
  9. 时间序列:简易prophet
  10. 工业用微型计算机(27)-dos和BIOS调用(1)