一文吃透 React 事件机制原理

大纲

主要分为4大块儿,主要是结合源码对 react事件机制的原理 进行分析,希望可以让你对 react事件机制有更清晰的认识和理解。

当然肯定会存在一些表述不清或者理解不够标准的地方,还请各位大神、大佬斧正。

01 – 对事件机制的初步理解和验证

02 – 对于合成的理解

03 – 事件注册机制

04 – 事件执行机制

01 02 是理论的废话,也算是我的个人总结,没兴趣的可以直接跳到 03-事件执行机制。

ps:本文基于 react15.6.1进行分析,虽然不是最新版本但是也不会影响我们对 react 事件机制的整体把握和理解。

对事件机制的初步理解和验证

react事件机制 的表象理解,验证,意义和思考。

表象理解

先回顾下 对react 事件机制基本理解,react自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。

我们都知道react 的所有事件并没有绑定到具体的dom节点上而是绑定在了document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的事件都会在 document 上触发。

既然已经有了对 react事件 的一个基本的认知,那这个认知是否正确呢?我们可以通过简单的方法进行验证。

验证

验证内容:

所有事件均注册到了元素的最顶层-document 上 节点的事件由统一的入口处理 为了方便,直接通过 cli 创建一个项目。

代码中给两个 button 绑定了合成事件,单独给 btn#btn-reactandnative 绑定了一个原生的事件。

然后看下 chrome 控制台,查看元素上注册的事件。

经过简单的验证,可以看到所有的事件根据不同的事件类型都绑定在了 document 上,触发函数统一是 dispatchEvent

试想一下

如果一个节点上同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样的呢?

其实读到这里答案已经有了。我们现在基于目前的知识去分析下这个关系。

因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent 统一去处理。

* 得出的结论: *

原生事件阻止冒泡肯定会阻止合成事件的触发。

合成事件的阻止冒泡不会影响原生事件。

为什么呢?先回忆下浏览器事件机制

浏览器事件的执行需要经过三个阶段,捕获阶段-目标元素阶段-冒泡阶段。

节点上的原生事件的执行是在目标阶段,然而合成事件的执行是在冒泡阶段,所以原生事件会先合成事件执行,然后再往父节点冒泡。

既然原生都阻止冒泡了,那合成还执行个啥嘞。

好,轮到合成的被阻止冒泡了,那原生会执行吗?当然会了。

因为原生的事件先于合成的执行,所以合成事件内阻止的只是合成的事件冒泡。(代码我就不贴了)

所以得出结论:

原生事件(阻止冒泡)会阻止合成事件的执行

合成事件(阻止冒泡)不会阻止原生事件的执行

两者最好不要混合使用,避免出现一些奇怪的问题

意义

react 自己做这么多的意义是什么?

  1. 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次

  2. 统一规范,解决 ie 事件兼容问题,简化事件逻辑

  3. 对开发者友好

思考

既然 react 帮我们做了这么多事儿,那他的背后的机制是什么样的呢?

事件怎么注册的,事件怎么触发的,冒泡机制怎样实现的呢?

请继续往后看……

对于合成的理解

刚听说合成这个词时候,感觉是特别高大上,很有深度,不是很好理解。

当我大概的了解过react事件机制后,略微了解一些皮毛,我觉得合成不单单是事件的合成和处理,从广义上来说还包括:

  1. 对原生事件的封装

  2. 对某些原生事件的升级和改造

  3. 不同浏览器事件兼容的处理

对原生事件的封装

上面代码是给一个元素添加 click 事件的回调方法,方法中的参数 e ,其实不是原生事件对象而是react包装过的对象,同时原生事件对象被放在了属性 e.nativeEvent 内。

通过调试,在执行栈里看下这个参数 e 包含哪些属性

再看下官方说明文档

SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。

react会根据当前的事件类型来使用不同的合成事件对象,比如鼠标单机事件 – SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,但是都是继承自SyntheticEvent。

对原生事件的升级和改造

对于有些dom元素事件,我们进行事件绑定之后,react并不是只处理你声明的事件类型,还会额外的增加一些其他的事件,帮助我们提升交互的体验。

这里就举一个例子来说明下:

当我们给input声明个onChange事件,看下 react帮我们做了什么?

可以看到react不只是注册了一个onchange事件,还注册了很多其他事件。

而这个时候我们向文本框输入内容的时候,是可以实时的得到内容的。

然而原生只注册一个onchange的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷react也帮我们弥补了。

ps:上面红色箭头中有一个 invalid事件,这个并没有注册到document上,而是在具体的元素上。我的理解是这个是html5新增的一个事件,当输入的数据不符合验证规则的时候自动触发,然而验证规则和配置都要写在当前input元素上,如果注册到document上这个事件就无效了。

浏览器事件的兼容处理

react在给document注册事件的时候也是对兼容性做了处理。

上面这个代码就是给document注册事件,内部其实也是做了对 ie浏览器 的兼容做了处理。

以上就是我对于react合成这个名词的理解,其实react内部还处理了很多,我只是简单的举了几个栗子,后面开始聊事件注册和事件派发的机制。

事件注册机制

这是 react 事件机制的第三节 – 事件注册,在这里你将了解 react 事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源码进行验证和增强理解。

在这里并不会说非常细节的内容,而是把大概的流程和原理性的内容进行介绍,做到对整体流程有个认知和理解。

大致流程

react 事件注册过程其实主要做了2件事:事件注册、事件存储。

a. 事件注册 – 组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange 等,给 document 上添加事件 -addEventListener,并指定统一的事件处理程序 dispatchEvent。

b. 事件存储 – 就是把 react 组件内的所有事件统一的存放到一个对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。

关键步骤

上面大致说了事件注册需要完成的两个目标,那完成目标的过程需要经过哪些关键处理呢?

首先 react 拿到将要挂载的组件的虚拟 dom(其实就是 react element 对象),然后处理 react dom 的 props ,判断属性内是否有声明为事件的属性,比如 onClick,onChange ,这个时候得到事件类型 click,change 和对应的事件处理程序 fn ,然后执行后面 3步

a. 完成事件注册

b. 将 react dom ,事件类型,处理函数 fn 放入数组存储

c. 组件挂载完成后,处理 b 步骤生成的数组,经过遍历把事件处理函数存储到 listenerBank(一个对象)

源码解析

从 jsx 说起

看个最熟悉的代码,也是我们日常的写法

经过 babel 编译后,可以看到最终调用的方法是 react.createElement ,z而且声明的事件类型和回调就是个 props

react.createElement 执行的结果会返回一个所谓的虚拟 dom (react element object)

处理组件props,拿到事件类型和回调 fn

ReactDOMComponent 在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):

可以看下 registrationNameModules 的内容,就不细说了,他就是一个内置的常量。

事件注册和事件的存储

事件注册

接着上面的代码执行到了这个方法

在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理

根据当前的组件实例获取到最高父级-也就是document,然后执行方法 listenTo – 也是最关键的一个方法,进行事件绑定处理。

源码文件:ReactBrowerEventEmitter.js

最后执行 EventListener.listen(冒泡) 或者 EventListener.capture(捕获) ,单看下冒泡的注册,其实就是 addEventListener 的第三个参数是 false

也可以看到注册事件的时候也对 ie 浏览器做了兼容。

上面没有看到 dispatchEvent 的定义,下面可以看到传入 dispatchEvent 方法的代码。

到这里事件注册就完事儿了。

事件存储

开始事件的存储,在 react 里所有事件的触发都是通过 dispatchEvent方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下如何存储的 。

react 把所有的事件和事件类型以及react 组件进行关联,把这个关系保存在了一个 map里,也就是一个对象里(键值对),然后在事件触发的时候去根据当前的 组件id和 事件类型查找到对应的 事件fn。

结合源码:

大致的流程就是执行完 listenTo(事件注册),然后执行 putListener 方法进行事件存储,所有的事件都会存储到一个对象中 – listenerBank,具体由 EventPluginHub进行管理。

listenerBank其实就是一个二级 map,这样的结构更方便事件的查找。

这里的组件 id 就是组件的唯一标识,然后和fn进行关联,在触发阶段就可以找到相关的事件回调。

看到这个结构是不是很熟悉呢?就是我们平常使用的 object.

到这里大致的流程已经说完,是不是感觉有点明白又不大明白。

没关系,再来个详细的图,重新理解下。

事件执行机制

在事件注册阶段,最终所有的事件和事件类型都会保存到 listenerBank中。

那么在事件触发的过程中上面这个对象有什么用处呢?

其实就是用来查找事件回调

大致流程

事件触发过程总结为主要下面几个步骤:

1.进入统一的事件分发函数(dispatchEvent)

2.结合原生事件找到当前节点对应的ReactDOMComponent对象

3.开始 事件的合成

3.1 根据当前事件类型生成指定的合成对象

3.2 封装原生事件和冒泡机制

3.3 查找当前元素以及他所有父级

3.4 在 listenerBank查找事件回调并合成到 event(合成事件结束)

4.批量处理合成事件内的回调事件(事件触发完成 end)

举个栗子

在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子

看到这个熟悉的代码,我们就已经知道了执行结果。

当我点击 child div 的时候,会同时触发father的事件。

源码解析

dispatchEvent 进行事件分发

进入统一的事件分发函数 (dispatchEvent)。

当我点击child div 的时候,这个时候浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。(上一文中我们已经说过 document 上已经注册了一个统一的事件处理函数 dispatchEvent)。

查找ReactDOMComponent

结合原生事件找到当前节点对应的 ReactDOMComponent对象,在原生事件对象内已经保留了对应的 ReactDOMComponent实例的引用,应该是在挂载阶段就已经保存了。

看下ReactDOMComponent实例的内容

事件合成ing

事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。

合成对象的生成

根据当前事件类型找到对应的合成类,然后进行合成对象的生成

封装原生事件和冒泡机制

在这一步会把原生事件对象挂到合成对象的自身,同时增加事件的默认行为处理和冒泡机制

下面是增加的默认行为和冒泡机制的处理方法,其实就是改变了当前合成对象的属性值, 调用了方法后属性值为 true,就会阻止默认行为或者冒泡。

看下 emptyFunction 代码就明白了

查找所有父级实例

根据当前节点实例查找他的所有父级实例存入path

看下 path 长啥样

事件合成结束

在listenerBank查找事件回调并合成到 event。

紧接着上面代码

上面的代码会调用下面这个方法,在 listenerBank 中查找到事件回调,并存入合成事件对象。

为什么能够查找到的呢?

因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系。

到这里事件合成对象生成完成,所有的事件回调已保存到了合成对象中。

批量处理事件合成对象

批量处理合成事件对象内的回调方法(事件触发完成 end)。

生成完 合成事件对象后,调用栈回到了我们起初执行的方法内。

到下面这一步中间省略了一些代码,只贴出主要的代码,下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。

贴上最后的执行回调方法的代码

最后react 通过生成了一个临时节点fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过fakeNode.dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。

到这里事件回调已经执行完成,但是也有些疑问,为什么在非生产环境需要通过自定义事件来执行回调方法。可以看下上面的代码在非生产环境对 ReactErrorUtils.invokeGuardedCallback 方法进行了重写。

总结

主要是从整体流程上介绍了下 react事件的原理,其中并没有深入到源码的各个细节,包括事务处理、合成的细节等,另外梳理过程中自己也有一些疑惑的地方,感觉说原理还能比较容易理解一些,但是一结合源码来写就会觉得乱,因为 react代码过于庞大,而且盘根错节,很难抽离,对源码有兴趣的小伙儿可以深入研究下,当然还是希望本文能够带给你一些启发,若文章有表述不清或有问题的地方欢迎留言、 交流、斧正。

参考资料

  • https://zhuanlan.zhihu.com/p/35468208

  • https://react.docschina.org/docs/events.html

回复“ 加群 ”与大佬们一起交流学习~

点这,与大家一起分享本文吧~