小结:
1)事件委托
在拥有1000个li元素的列表上,点击每一个li输出对应的文本内容,我们可以给每个li元素绑 定一个点击动作,那我们需要写很多重复的代码,不够优雅,内存开销也很大。
还有个办法可以满足我们的需求:
将事件绑定在ul标签上,当li标签被触发后,事件冒泡到父级元素ul,触发绑定上面的click事 件,通过事件对象中的target属性,可以获取到真正触发事件的li元素。
2)对所有事件中心化管理
可以借鉴事件委托这个概念实现对所有事件的中心化管理,并且在这个过程处理一下抹平一下浏览器间的差异,优化一下性能,干预一下事件的派发,设置事件优先级,高优任务高优执行,提高用户体验。
从onClick看react事件系统 https://mp.weixin.qq.com/s?__biz=MzIyMTg0OTExOQ==&mid=2247488331&idx=5&sn=937eeec0cbcac1b2374cc352d4f82f6c&chksm=e837203bdf40a92dacbd9e12a668626ced8255b8f7c220efe3210bbcb1279a0c41855620d2e1&scene=132#wechat_redirect
用react开发的过程中,我们常常写好多事件,onClick这个事件是非常常用的。
onClick这个不同于dom事件的绑定方式的写法,却又能达到我们想要的交互,那你知道react内部是怎么处理的吗?它特殊的事件机制是什么?又是怎么工作的呢?
我们今天就从onClick出发,从源码层面展开说说(本文针对的react16.13.1版本)
首先我们来看一下原生DOM绑定click事件:
接着来看一下在控制台中Event Listeners 中绑定情况
可以观察到click事件回调是绑定在button的handler上,点击button后触发的是这个handler 上存储的回调。
那我们接着来看一下react中的button绑定事件,绑定用的是onClick
看一下控制台中的表现
可以观察到button 和document 上都绑定了click ,但是button 的绑定的handler 绑定的 是noop() , 展开的话,它其实是个空函数,那我们的绑定的回调去哪里了呢?
因为button的click具有冒泡的特性,点击后冒泡到document,会触发绑定到document上的事件,它上面的handler上绑定了dispatchDiscreteEvent , 也就是说在react中button点击会触发document上的dispatchDiscreteEvent 方法,button点击后真正执行的是 document上的对应事件的绑定的回调,这就是的react事件的触发机制。
那它是怎么做的呢?
一、事件委托 & 合成事件
在进入正题之前,我们先温习一个概念 —— 事件委托
举一个很有名的栗子:
在拥有1000个li元素的列表上,点击每一个li输出对应的文本内容,我们可以给每个li元素绑 定一个点击动作,那我们需要写很多重复的代码,不够优雅,内存开销也很大。
还有个办法可以满足我们的需求:
将事件绑定在ul标签上,当li标签被触发后,事件冒泡到父级元素ul,触发绑定上面的click事 件,通过事件对象中的target属性,可以获取到真正触发事件的li元素。
这给了react灵感,既然已经采用了jsx 这种需要babel 编译的特殊语法了,并且存在内存中的是一个大的fiber对象,那对事件这块多做一些处理就很方便啊。
比如可以借鉴事件委托这个概念实现对所有事件的中心化管理,并且在这个过程处理一下抹平一下浏览器间的差异,优化一下性能,干预一下事件的派发,设置事件优先级,高优任务高优执行,提高用户体验。
怎么做呢?
当事件在具体的DOM节点上被触发后,会冒泡到#document(17+版本是root根节点)上(除 少数特殊的不可冒泡的事件,使用捕获),触发委托在上面的事件,#document上所绑定的统一事件处理程序会将事件分发到具体的组件实例,然后处理对应的回调
这个处理过程将原生的DOM事件处理成了react的合成事件,它具有和原生对象一样的接口,同样支持事件的冒泡捕获机制,还做一些措施简化了事件处理和回收机制,效率可以有了很大的提升。
我们从react源码内部看一下
二、React实现细节
我们知道react使用的是jsx语法,需要经过babel编译才能被使用,那我们的绑定有onClick 事件的demo编译后是什么呢?
通过React.createElement 方法会创建一个fiber节点,通过这个调用,会将整个react页面构建成一棵具有链式特性的fiber树。
我们有绑定事件的button就挂在在div对应的fiber结构的child上,下图就是它的结构:
Fiber对象上memoizedProps和pendingProps保存了我们的click事件,支持我们通过Fiber树找到对象的回调。
具体实现
这个流程图来源react 16.13.1仓库,那我们从整体看一下react的实现:
1)Dom 事件发生后,React通过事件委托机制将大部分事件代理至Document/ root层
2)ReactEventListener负责给事件注册和绑定
3)ReactEventEmitter暴露接口给react组件用于添加事件订阅(负责每个组件上事件的执行)
4)PluginRegistry负责根据不同的事件类型,构造不同的合成事件,比如simpleEventPlugin处理的click这类离散事件,将其合成对应的合成事件
5)应用到application上
要想了解一下它的具体实现,我们按照事件注册->事件绑定->事件触发这个顺序展开说说。
1、事件注册
注册环节主要做了三件事情:事件插件注入、辅助函数注入、定义生成全局变量(派发等流程要用)
事件插件注入
主要通过injectEventPluginOrder
与injectEventPluginsByName 两个函数完成
插件注入的特点是:
1)默认执行
2)一开始就加载了所有的插件
3)每个插件是单独模块,只处理自己对应的合成事件(代码解耦)
辅助函数注入
主要通过legacy-events/EventPluginUtils.js文件里面的setComponentTree 函数完成的 主要注入了三个核心辅助函数:
1)getInstanceFromNode :获取指定DOM节点对应fiber节点
2)getFiberCurrentPropsFromNode :获取指定DOM节点对应的事件监听器数据
3)getNodeFromInstance :获取指定fiber节点对应的DOM节点
全局变量
eventPluginOrder : 事件插件执行顺序定义
registrationNameDependencies : react事件和它对应的原生DOM事件映射
eventNameDispatchConfigs :React事件名称到React事件派发配置映射
plugins : 事件插件集合(按照上面eventPluginOrder)
registrationNameModules :react事件注册名称和它对应事件插件的映射
我们来划一下注册环节的重点:
1)初始化了几个对外变量,用于后面的事件派发
2)构建了react事件-事件插件、react事件-react事件派发、react事件-原生事件的映射关系
3)定义了事件处理过程当中需要使用的三个核心辅助函数
2、事件绑定
我们来看一下事件绑定发生的时机:
应用初始化调用ReactDOM.render ,经过react-reconciler处理 , 会形成fiber树,处理props, 如果props中涉及事件的话,会触发事件的绑定链路,
比如存储事件监听器和绑定事件。
具体的流程:
划一下React绑定环节的重点
1)标签默认事件react会帮我们绑定
2)事件会通过特定方法绑定到document上
3)react维护了一个变量listenerMap,保证同一个类型的事件 React 只会绑定一次原生事件。
例如无论我们写了多少个onClick, 最终反应在 DOM 事件上只会有一个listener, react只是确保自己能捕捉到,想当于全局做了一次事件委托。
4)大部分事件都按照冒泡逻辑处理,少数事件会按照捕获逻辑处理
5)按照优先级给不同的事件绑定不同的listener
6)经过事件调度后绑定的原生事件的listener都是dispatchEvent函数,进行统一的事件派发
展开解释一下:
优先级:
在React中人为地将事件划分了等级,最终目的是决定调度任务的轻重缓急,它有一套从事件到调度的优先级机制。
几种优先级环环相扣,保证了高优任务的优先执行
事件分类
由事件本身将事件分为离散事件、用户堵塞事件、连续事件
1)离散事件DiscreteEvent
代表事件:click、keydown、focusin、blur等
特点是触发不是连续的,优先级为0,
对应的事件Listener=dispatchDiscreteEvent(入参)
2)用户堵塞事件UserBlockingEvent
代表事件:touchMove、mouseMove、scroll、drag等,
特点是连续触发,阻塞渲染,优先级为1,
对应的Listener=dispatchUserBlockingUpdate(入参)
3)连续事件ContinuousEvent
代表事件load、error、loadStart、animation等,
特点是优先级最高,持续执行,不可打断,优先级2,对应的Listener= dispatchEvent(入参)
它们做的事情都是一样的,以各自的事件优先级去执行真正的事件处理函数,优先级值越大越高,会对应不同listener,入参都是一样
比如
dispatchDiscreteEvent.bind(null,topLevelType, PLUGIN_EVENT_SYSTEM, container)
以我们的demo中click为例子,它属于离散事件,对应的Listener是dispatchDiscreteEvent
展开这个函数看一下:
真正的事件源对象event(react中叫nativeEvent),被默认绑定成第四个参数 (addEventListener机制),这可以解释this需要bind绑定的原因。
通过调度将处理之前未处理完的DiscreteEvent 与useEffect 回调执行完后,当前click事件以最高优先级开始执行,就是说它以连续事件的同等的优先级开始执行,对应的listener 是dispatchEvent , 这也解释了绑定流程中的最后一步,最后执行是dispatchEvent 这个派发函数
3、事件触发
上文我们提到过,react事件会经过事件插件的处理,一个事件插件会处理一类的事件,以 demo中onClick为例,它属于离散事件,处理离散事件的是SimpleEventPlugin 。
为了大家有直观的认识,我们展开看一下:
核心处理函数是extractEvents ,我们看一下它的入参和return:
入参:
return的是合成事件的event
除了继承了原生event的属性,还添加了两个_dispatchListeners 和_dispatchInstances
_dispatchListeners :存储的是事件触发过程中,按照先父捕获->子捕获->子冒泡->父冒泡 顺序搜集的事件回调的集合,比如我们demo中的handler 。
_dispatchInstances :存储是_dispatchListeners 顺序对应的fiber节点
我们来总结一下事件插件做的事情:
1)构造SyntheticEvent 对象(合成event)
2)获取执行回调队列,挂在SyntheticEvent 的_dispatchListeners 属性上
3)回调队列对应的fiebr数组,挂在SyntheticEvent 的_dispatchInstances 属性上
话不多说,上图,我们以demo中click看事件触发。
button点击触发后,冒泡到document上,触发绑定的handle函数dispatchDiscreteEvent ,经过事件调度最终执行的是dispatchEvent , 这个函数开始派发具体的触发实例。
有四个入参,通过注册环节过程注入的全局函数和核心处理函数,可以找到绑定事件的dom 节点,对应的fiber节点,和对应的事件插件。
然后事件插件会按照fiber树的结构搜集button 点击后(捕获->事件源->冒泡)会触发的所有回调和对应的fiber,然后开始执行回调队列。
那拿到事件插件处理后的事件回调队列是怎么执行的呢?
我们demo中绑定的回调就是在这里执行的,通过for遍历执行,执行前会判断一下是否需要阻止冒泡,如果是的话就break这次for循环,否的话,就一直执行dispatchListeners[i](event) (不得不说for循环yyds),它就是button点击后绑定的最终执行的函数。
回调队列执行完成,会将这两个的属性的值置为null。
那么问题了,在demo中这两种写法,哪种是有效的呢?
答案是第一种,写在e上的方法react的SyntheticEvent 会继承并且识别处理,第二种在真正 执行的for循环中没有意义。
以上就是react事件机制这块完整的流程,感兴趣的童鞋,可以参考 https://github.com/Terry-Su/debug-react-source-code这里面的debugger方式,然后配着 本文的流程图中的内容研究更佳哦~
小结:1)事件委托在拥有1000个li元素的列表上,点击每一个li输出对应的文本内容,我们可以给每个li元素绑 定一个点击动作,那我们需要写很多重复的代码,不够优雅,内存开销也很大。
还有个办法可以满足我们的需求:
将事件绑定在ul标签上,当li标签被触发后,事件冒泡到父级元素ul,触发绑定上面的click事 件,通过事件对象中的target属性,可以获取到真正触发事件的li元素。
2)对所有事件中心化管理可以借鉴事件委托这个概念实现对所有事件的中心化管理,并且在这个过程处理一下抹平一下浏览器间的差异,优化一下性能,干预一下事件的派发,设置事件优先级,高优任务高优执行,提高用户体验。