摘要:二阻止事件冒泡,没办法阻止原生事件冒泡。从事件的三个阶段讲起,会略微提及下的事件机制。就是说注册某个事件,会强制依赖其他事件。因为所有事件都是绑定在上的。划重点事件合成的过程。派发的过程实际上就是遍历事件队列的过程。
react.js事件机制
写这篇文章的缘由:一:在给input绑定事件的时候,很好奇为何onChange的交互形式竟然和onInput一模一样。 因为原生的change事件是在input失去焦点的时候触发,但react的onChange则完全不同。 二:阻止事件冒泡,event.preventDeault()没办法阻止原生事件冒泡。从react事件的三个阶段讲起,会略微提及下react15的事件机制。
一 事件注册 二 事件合成 三 事件派发react事件注册
1. 一切从createInstance,创建dom实例开始讲起。
react16引入了fiber的概念,讲fiber的文章很多,这里就不多阐述。可以简单的先理解为fiber Tree 略微等于 vDOM Tree(当然实际肯定有差别)。
当react遍历tree创建真实dom实例的时候做了什么???
重点就在这几行代码。domElement等于真实创建的dom,里面调用的就是我们熟悉的createElement。而precacheFiberNode和updateFiberProps两个方法分别给domElement(真实dom)添加了两个属性。所有react16项目中的dom都会拥有这两个属性,并且这个两个属性的属性名在同一个项目中是一致的。
图片描述
precacheFiberNode方法中 设置 node[internalInstanceKey] = 一个new FiberNode()的实例
updateFiberProps方法中 设置node[internalEventHandlersKey] = props 。这里的props就是
划重点了! 这两个方法等于将真实dom和fiber,props直接关联到了一起,相互引用,这点很重要,后续会用到,相当于前期准备。
2. listenTo
createInstance后(仍然在fiber tree遍历中),程序兜兜转转,层层调用,最后终于走到了关键方法listenTo这里来,所有的事件注册逻辑都在这里实现。react做了一系列的兼容处理,尽可能的保证各个浏览器端交互一致。
ensureListeningTo里面调用的就listenTo。首先去遍历props中的属性。而registrationNameModules.hasOwnProperty(propKey),registrationNameModules是一个事件名的集合,几乎包含了所有的常见事件,这也就是如果你写一些稀奇古怪的事件,react是不识别的。如果判定props中的属性 如onClick在registrationNameModules中,并且值typeof === function,则会进入到listenTo中。
回到listenTo这个方法,他接收两个参数,一个是事件名如onClick,一个是contentDocumentHandle,通常就是document。
重点讲下这个 var dependencies = registrationNameDependencies[registrationName]; registrationNameDependencies这个东西理解为事件依赖。 就是说注册某个事件,react会强制依赖其他事件。而具体是哪些依赖,react的event模块已经帮我们处理了,就不深层次探讨。
举例onChange事件就依赖了下面的一些事件
registrationNameDependencies = {
onChange = ["topBlur", "topChange", "topClick", "topInput", "topKey" ....还有]
}
这里react的事件都加上了top前缀,没什么太大的深层次含义,可能就是为了区分下吧,毕竟后续用到的时候,react会再次把它转回来的。如topInput转成input之类的。
接着往下走dependencies得到的是一个依赖事件数组,随即遍历这个数组,做一些hack处理,然后会调用这个方法trapBubbledEvent。
trapBubbledEvent(dependency, topLevelTypes[dependency], mountAt) ,三个参数 dependency= topClick。 上面提到过的,会把top前缀什么的再次转回来, 所以topLevelTypes[dependency]就是click。mountAt就等于document(不考虑多个window)。那trapBubbledEvent又做了什么呢?
这里说白就是调用下面这个方法
熟悉了吧。这就是我们常见的事件绑定了。。
又到划重点时间了
listenTo做的事情很简单,就是遍历props中的event,然后将事件和事件的依赖事件统统挂载到document上,并且所有的事件的回调函数走的都是dispatchEvent。
打个比方,如果我绑定一个onChange事件,那么react不仅仅只绑定一个onChange事件到document上,还会绑定许多依赖事件上去,如focus,blur,input等等。是不是看出点什么苗头。onChange事件依赖了onInput事件,但这还并不是 为什么onChange的表现形式和onInput一样的 全部原因。 这只是为后续的合成提供了依赖。
整个事件注册差不多就到这里为止了。react会把所有的事件都挂载到document上。这也是为什么我们event.stoppropagation()为什么不能阻止原生冒泡。因为所有事件都是绑定在document上的。意味着你的原生事件都执行完了之后,才能执行document的事件。dispatchEvent会做统一的派发。可以说原生事件的执行顺序是早于react事件的。
稍微提及下react15的事件注册和16完全不同,16有一个listenBink的感念,所以的事件都注册到这个对象里面,并且由react_id关联起来。但16已经舍弃了react_id的感念。
react事件合成以一个input输入框为例,当用户输入了数字1之后,react做了什么。
首先回到上面说的disPatchEvent中。所有的事件回调统统都是这个函数,现在我们看看这个函数做了什么
dispatchEvent接收两个参数 topLevelType(事件topFocus之类的) nativeEvent就是原生的event对象。
dispatchEvent里面绕得有点深,跳来跳去。最后会走到下面这个方法里面来
handleTopLevel接收4个参数,targetInst就是fiber实例,上面提到过每个dom中都会挂载两个属性,其中一个保存了fiber的引用。过程就是根据dispatchEvent得到的一个nativeEvent,可以得到
一个真正触发事件的nativeEventTarget元素(event.target),然后取得fiber引用即可。 这样handleTopLevel四个关键参数都齐全了。
handleTopLevel又做了什么呢。。根据里面方法的字面意思 无非是提取event对象(react的合成对象,并非原生的event),然后放到事件队里去。
重点来说说extractEvents方法,所有的奥秘都在这里了。。。它接收了handleTopLevel的四个参数。
这里可能有点绕,需要理解下plugins。简单的解释就是react的event模块所包含的eventPliguns。好像有6个左右的plugin吧,或许是不同的组件处理不同的事件类型吧。具体实现和功能就没必要说了,太底层。
比如用户在input输入的过程,或许第一步是触发了某个元素的blur,然后是input的focus,然后是keydown,input之类等等,顺序就是按照浏览器的事件顺序。
我们拿input事件举例,撇开其他无关事件(注:这里会解释 最开始提到的第一个问题)。
。
划重点了。。
上面的Input,虽然我们只注册了一个onChange,但根据我们前面的了解,react会注册依赖事件,onChange会依赖onInput,因此同样会注册onInput事件。
当input事件被触发的时候,遍历plugin去处理事件。并返回一个由plugin合成的event
events = accumulateInto(events, extractedEvents); 看这里,events是一个数组,accumulateInto等同于events.push(extractedEvents);
重点来 plugin处理onInput事件的时候,会生成两个event,一个是input,一个是change, 会生成两个event,一个是input,一个是change, 会生成两个event,一个是input,一个是change,
重要的事情说三遍,记住这个地方。后面是关键。
回到plugin这里来,进入到possiblePlugin.extractEvents里面去,这个函数返回一个event,并且在层层调用后执行了一个至关重要的函数traverseTwoPhase ,让我们走进去看看这个函数
究竟做了什么?
traverseTwoPhase接收三个参数,inst = fiber实例 fn = accumulateDirectionalDispatches函数,等会会着重讲解这个方法。arg = event。这个event是possiblePlugin.extractEvents中生成的event对象,它把这个event当参数传递到更深层的方法里面,是为了在event上挂载两个极为重要的属性,等下细说。
traverseTwoPhase 具体做了什么呢?
while循环,取到fiber(触发事件的真正target所对应的虚拟dom)的所有父节点,等同于得到了一棵fiberTree。
如下
{console.log(1111)}}> {console.log(2222)}}> {console.log(3333)}} />
假设有上面三个组件ComponentA, ComponentB, ComponentC。层层嵌套。那么path就等于[ComponentC, ComponentB, ComponentA];
traverseTwoPhase 中执行了两次循环,一次为captured捕获,即执行顺序从A-C。一次为bubbled冒泡,执行顺序为从C-A。这里我们不考虑captured。详细讲解下冒泡过程。上面
说过了fn = accumulateDirectionalDispatches,我们看看这里到底做了什么
重点看这里 var listener = listenerAtPhase(inst, event, phase); 这里就是取当前的dom有没有注册对应事件的listener。粗略解释下取得的过程就是利用了最上面说过的绑定在dom
上的props,如果listener存在,则将当前的listener和inst(理解为虚拟dom或者fiber实例)分别挂载到event下的两个数组里。 这里极其重要
注意啦。 accumulateDirectionalDispatches 这个函数是在 path的循环里执行的。回到上面的path等于[ComponentC, ComponentB, ComponentA]的例子。因为我们的ABC三个组件都注册了对应的listener,故而event下的_dispatchListeners和_dispatchInstances都存储有其inst和listner。至此event算是合成完毕了,控制权回到possiblePlugin.extractEvents这里。
划重点 : 事件合成的过程。首先根据触发事件的target得到inst,然后遍历他的所有父节点(fiber.return属性),存储在局部遍历path中,记住这个path是有顺序关系的(后面可以解释react事件是如何阻止冒泡的)。得到path后进行遍历,假设遍历的组件同样注册了对应事件的listener,那么就挂载到event的_dispatchListeners和_dispatchInstances中去,这两个属性至关重要,后续的事件派发就是根据这两个属性进行的。 注意只有注册了对应事件的listener,才会挂载到event里面去。比如刚刚我们的ABC都绑定了Click,自然都会push到_dispatchListeners中去。
回忆下上面刚刚说到的一个很绕的地方。 假设我给一个input绑定了onChange事件,那么react会绑定很多依赖事件到document上。其中就有input事件。 但当我们触发input的时候,react是怎么触发到onChange的listener呢? 。重点就是刚刚说了三遍的地方,在合成input事件的时候,react会生成两个event,一个是input,一个是change,也就是说change的那个event挂载的_dispatchListeners里面存储了我们的listener。后续派发的时候,会执行这个事件队列,对 队列里的event进行派发。 这也就很好的解释了为什么我们的change交互和input一模一样。
事件派发事件合成之后就是事件派发了,虽然我分开来讲,当两个过程是紧跟着的,合成事件后,所有的事件都会push到eventQueue里面去。派发的过程实际上就是遍历事件队列的过程。
遍历的过程同样有点绕,我们只看关键的地方
有么有看到我们熟悉的地方。_dispatchListeners和_dispatchInstances这两个里面保存了我们的inst和listener。**方法对dispatchListeners进行了遍历,event.isPropagationStopped()
这个地方也就是我们可以阻止合成事件冒泡的原因呢。。** 因为dispatchListeners同样是按照冒泡的顺序插入的,就拿刚刚的ABC三个组件来说。假设对B进行了阻止冒泡。那么A的onClick就没办法
执行了。
遍历过程中拿到inst和对应的listener后,执行executeDispatch,后续的代码就简单直接了。
创建了个虚假的dom,绑定个自定义事件,然后再自己监听,再自己createEvent,再dispatch,触发callback,然后在callback里面触发真正的listener,同时会把合成的event传递进去。
最后在清空重置各种数据。。大结局了。
事件注册,事件合成,事件派发,三个阶段,这里就不再总结了,每一阶段后都有个总结的。
讲真react代码有点复杂,看不到的多看几遍吧。
写的不对的地方指正下。有不懂的提问。。码这么多不容易了,可能有些单词拼错了,包容下。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/107906.html
摘要:前言这是事件机制的第一篇,主要内容有表象理解,验证,意义和思考。因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由统一去处理。合成事件的阻止冒泡不会影响原生事件。 showImg(https://segmentfault.com/img/bVbtvP2?w=800&h=420); 前言 这是 react 事件机制的第一篇,主要内容有:表象理解,验证...
摘要:前言这是事件机制系列文章的第二篇对于合成的理解,咱们就来说说合成这个名词。在给注册事件的时候也是对兼容性做了处理。总结以上就是我对于合成这个名词的理解,其实内部还处理了很多,我只是略微简单的举了几个栗子。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 这是react事件机制系列文章的第二篇-对于合成的理解,...
摘要:对事件机制的初步理解和验证对于合成的理解事件注册机制事件执行本文基于进行分析,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。最后希望通过本文可以让你对事件机制有更清晰的认识和理解。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 写这个文章也算是实现19年的一个 flag,研究一个知识点并且把...
摘要:文章涉及到的源码是基于版本,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。到这里事件注册就完事儿了。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 这是 react 事件机制的第三节 - 事件注册,通过本文你将了解react 事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源...
摘要:文章涉及到的源码是基于版本,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。总结本文主要是从整体流程上介绍了下事件触发的过程。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 这是 react 事件机制的第四节-事件执行,一起研究下在这个过程中主要经过了哪些关键步骤,本文也是react 事件机制...
阅读 2901·2021-11-25 09:43
阅读 2319·2021-11-24 09:39
阅读 2707·2021-09-23 11:51
阅读 1399·2021-09-07 10:11
阅读 1448·2019-08-27 10:52
阅读 1929·2019-08-26 12:13
阅读 3355·2019-08-26 11:57
阅读 1392·2019-08-26 11:31