React事件系统研究总结

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 端音视频测试>>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值