内容简介:本文以 React v16.5.2 为基础进行源码分析在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁,至于下面的结构图形,则是对事件机制的一个图形化描述
本文以 React v16.5.2 为基础进行源码分析
基本流程
在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:
事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁,至于下面的结构图形,则是对事件机制的一个图形化描述
- React事件使用了事件委托的机制,一般事件委托的作用都是为了减少页面的注册事件数量,减少内存开销,优化浏览器性能,React这么做也是有这么一个目的,除此之外,也是为了能够更好的管理事件,实际上,React中所有的事件最后都是被委托到了 document这个顶级DOM上
- 既然所有的事件都被委托到了 document上,那么肯定有一套管理机制,所有的事件都是以一种先进先出的队列方式进行触发与回调
- 既然都已经接管事件了,那么不对事件做些额外的事情未免有些浪费,于是 React中就存在了自己的 合成事件(SyntheticEvent),合成事件由对应的 EventPlugin负责合成,不同类型的事件由不同的 plugin合成,例如 SimpleEvent Plugin、TapEvent Plugin等
- 为了进一步提升事件的性能,使用了 EventPluginHub这个东西来负责合成事件对象的创建和销毁
#开始
<button onClick={this.autoFocus}>点击聚焦</button> 复制代码
这是我们在React中绑定事件的常规写法。经由JSX解析,button会被当做组件挂载。而onClick这时候也只是一个普通的props。 ReactDOMComponent在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):
事件注册
ReactDOMComponent.Mixin = { mountComponent:function(){}, _createOpenTagMarkupAndPutListeners:function(){}, ...., // 方法中有指向上次属性值得lastProp, // nextProp是当前属性值,这里nextProp是我们绑定给组件的onclick事件处理函数。 // nextProp 不为空调用enqueuePutListener绑定事件,为空则注销事件绑定。 _updateDOMProperties:function(lastProps, nextProps, transaction){ for (propKey in lastProps) {} //省略。。。 for (propKey in nextProps) { // 判断是否为事件属性 if (registrationNameModules.hasOwnProperty(propKey)) { enqueuePutListener(this, propKey, nextProp, transaction); } } } } //这里进行事件绑定 首先判断了 rootContainerElement是不是一个 document或者 Fragment(文档片段节点) enqueuePutListener 这个方法只在浏览器环境下执行,传给listenTo参数分别是事件名称'onclick'和代理事件的绑 定dom。如果是fragement 就是根节点(在reactDom.render指定的),不是的话就是document。listenTo 用于绑定事件到 document ,下面交由事务处理的是回调函数的存储,便于调用。 ReactBrowserEventEmitter 文件中的 listenTo 看做事件处理的源头。 这里获取了当前组件(其实这时候就是button)所在的document function enqueuePutListener(inst, registrationName, listener, transaction) { ... var containerInfo = inst._hostContainerInfo; var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE; var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; listenTo(registrationName, doc); ... } 复制代码
绑定的重点是这里的listenTo方法。看源码(ReactBrowerEventEmitter)
//registrationName:需要绑定的事件 //当前component所属的document,即事件需要绑定的位置 listenTo: function (registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; //获取当前document上已经绑定的事件 var isListening = getListeningForDocument(mountAt); // 获取 registrationName(注册事件名称)的topLevelEvent(顶级事件类型) var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName]; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { if (dependency === 'topWheel') { ... } else if (dependency === 'topScroll') { ... } else if (dependency === 'topFocus' || dependency === 'topBlur') { ... } else if (topEventMapping.hasOwnProperty(dependency)) { // 获取 topLevelEvent 对应的浏览器原生事件 //冒泡处理 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt); } isListening[dependency] = true; } } }, 复制代码
对于同一个事件,例如click有两个事件 onClick(在冒泡阶段触发) onClickCapture(在捕获阶段触发)两个事件名,这个冒泡和捕获都是react事件模拟出来的。绑定到 document上面的事件基本上都是在冒泡阶段(对 whell, focus, scroll 有额外处理),如下图 click 事件绑定执行的如下。
最后处理(EventListener的listen和capture中)
//eventType:事件类型,target: document对象, //callback:是固定的,始终是ReactEventListener的dispatch方法 if (target.addEventListener) { target.addEventListener(eventType, callback, false); return { remove: function remove() { target.removeEventListener(eventType, callback, false); } }; } 复制代码
所有事件绑定在document上
所以事件触发的都是ReactEventListener的dispatch方法
回调储存
看到这边你可能疑惑,所有回调都执行的ReactEventListener的dispatch方法,那我写的回调干嘛去了。别急,接着看:
function enqueuePutListener(inst, registrationName, listener, transaction) { ... //注意这里!!!!!!!!! //这里获取了当前组件(其实这时候就是button)所在的document var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; //事件绑定 listenTo(registrationName, doc); //这段代码表示将putListener放入回调序列,当组件挂载完成是会依次执行序列中的回调。putListener也是在那时候执行的。 //不明白的可以看看本专栏中前两篇关于transaction和挂载机制的讲解 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); //保存回调 function putListener() { var listenerToPut = this; EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); } } 复制代码
还是这段代码,事件绑定我们介绍过,主要是listenTo方法。 当绑定完成以后会执行putListener。该方法会在ReactReconcileTransaction事务的close阶段执行,具体由EventPluginHub来进行管理
// var listenerBank = {}; var getDictionaryKey = function (inst) { //inst为组建的实例化对象 //_rootNodeID为组件的唯一标识 return '.' + inst._rootNodeID; } var EventPluginHub = { //inst为组建的实例化对象 //registrationName为事件名称 //listner为我们写的回调函数,也就是列子中的this.autoFocus putListener: function (inst, registrationName, listener) { ... var key = getDictionaryKey(inst); var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[key] = listener; ... } } 复制代码
EventPluginHub在每个项目中只实例化一次。也就是说,项目组所有事件的回调都会储存在唯一的listenerBank中。
是不是有点晕,放上流程图,仔细回忆一下
事件触发
注册事件时我们说过,所有的事件都是绑定在Document上。回调统一是ReactEventListener的dispatch方法。 由于冒泡机制,无论我们点击哪个DOM,最后都是由document响应(因为其他DOM根本没有事件监听)。也即是说都会触发dispatch
dispatchEvent: function(topLevelType, nativeEvent) { //实际触发事件的DOM对象 var nativeEventTarget = getEventTarget(nativeEvent); //nativeEventTarget对应的virtual DOM var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode( nativeEventTarget, ); ... //创建bookKeeping实例,为handleTopLevelImpl回调函数传递事件名和原生事件对象 //其实就是把三个参数封装成一个对象 var bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, ); try { //这里开启一个transactIon,perform中执行了 //handleTopLevelImpl(bookKeeping) ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); } }, 复制代码
function handleTopLevelImpl(bookKeeping) { //触发事件的真实DOM var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); //nativeEventTarget对应的ReactElement var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget); //bookKeeping.ancestors保存的是组件。 var ancestor = targetInst; do { bookKeeping.ancestors.push(ancestor); ancestor = ancestor && findParent(ancestor); } while (ancestor); for (var i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; //具体处理逻辑 ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); } } 复制代码
//这就是核心的处理了 handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { //首先封装event事件 var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); //发送包装好的event runEventQueueInBatch(events); } 复制代码
事件封装
首先是EventPluginHub的extractEvents
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { var events; var plugins = EventPluginRegistry.plugins; for (var i = 0; i < plugins.length; i++) { // Not every plugin in the ordering may be loaded at runtime. var possiblePlugin = plugins[i]; if (possiblePlugin) { //主要看这边 var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); ...... } } return events; }, 复制代码
接着看SimpleEventPlugin的方法
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { ...... //这里是对事件的封装,但是不是我们关注的重点 var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget); //重点看这边 EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 复制代码
接下来是方法中的各种引用,跳啊跳,转啊转,我们来到了ReactDOMTraversal中的traverseTwoPhase方法
//inst是触发事件的target的ReactElement //fn:EventPropagator的accumulateDirectionalDispatches //arg: 就是之前部分封装好的event(之所以说是部分,是因为现在也是在处理Event,这边处理完才是封装完成) function traverseTwoPhase(inst, fn, arg) { var path = []; while (inst) { //注意path,这里以ReactElement的形式冒泡着, //把触发事件的父节点依次保存下来 path.push(inst); //获取父节点 inst = inst._hostParent; } var i; //捕捉,依次处理 for (i = path.length; i-- > 0;) { fn(path[i], 'captured', arg); } //冒泡,依次处理 for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); } } 复制代码
//判断父组件是否保存了这一类事件 function accumulateDirectionalDispatches(inst, phase, event) { //获取到回调 var listener = listenerAtPhase(inst, event, phase); if (listener) { //如果有回调,就把包含该类型事件监听的DOM与对应的回调保存进Event。 //accumulateInto可以理解成_.assign //记住这两个属性,很重要。 event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } 复制代码
listenerAtPhase里面执行的是EventPluginHub的getListener函数
getListener: function (inst, registrationName) { //还记得之前保存回调的listenerBank吧? var bankForRegistrationName = listenerBank[registrationName]; if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) { return null; } //获取inst的_rootNodeId var key = getDictionaryKey(inst); //获取对应的回调 return bankForRegistrationName && bankForRegistrationName[key]; }, 复制代码
事件分发
runEventQueueInBatch主要进行了两步操作
function runEventQueueInBatch(events) { //将event事件加入processEventQueue序列 EventPluginHub.enqueueEvents(events); //前一步保存好的processEventQueue依次执行 //executeDispatchesAndRelease EventPluginHub.processEventQueue(false); } processEventQueue: function (simulated) { var processingEventQueue = eventQueue; eventQueue = null; if (simulated) { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); } else { //重点看这里 //forEachAccumulated可以看成forEach的封装 //那么这里就是processingEventQueue保存的event依次执行executeDispatchesAndReleaseTopLevel(event) forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); } }, 复制代码
executeDispatchesAndReleaseTopLevel(event)又是各种函数包装,最后干活的是
function executeDispatchesInOrder(event, simulated) { //对应的回调函数数组 var dispatchListeners = event._dispatchListeners; //有eventType属性的ReactElement数组 var dispatchInstances = event._dispatchInstances; ...... if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { if (event.isPropagationStopped()) { break; } executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); } } else if (dispatchListeners) { executeDispatch(event, simulated, dispatchListeners, dispatchInstances); } event._dispatchListeners = null; event._dispatchInstances = null; } 复制代码
OK,这里总算出现了老熟人,在封装nativeEvent时我们保存在event里的两个属性,dispatchListeners与dispatchInstances,在这里起作用。 代码很简单,如果有处理这个事件的回调函数,就一次进行处理。细节我们稍后讨论,先看看这里是怎么处理的吧
function executeDispatch(event, simulated, listener, inst) { //type是事件类型 var type = event.type || 'unknown-event'; //这是触发事件的真实DOM,也就是列子中的button event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { //看这里看这里 ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; } 复制代码
终于来到最后了,代码位于ReactErrorUtil中 (为了帮助开发,React通过模拟真正的浏览器事件来获得更好的devtools集成。这段代码在开发模式下运行)
//创造一个临时DOM var fakeNode = document.createElement('react'); ReactErrorUtils.invokeGuardedCallback = function (name, func, a) { //绑定回调函数的上下文 var boundFunc = func.bind(null, a); //定义事件类型 var evtType = 'react-' + name; //绑定事件 fakeNode.addEventListener(evtType, boundFunc, false); //生成原生事件 var evt = document.createEvent('Event'); //将原生事件处理成我们需要的类型 evt.initEvent(evtType, false, false); //发布事件---这里会执行回调 fakeNode.dispatchEvent(evt); //移出事件监听 fakeNode.removeEventListener(evtType, boundFunc, false); }; 复制代码
以上所述就是小编给大家介绍的《react源码-事件监听》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)
- Glide 源码解析之监听生命周期
- 简单看,tomcat源码(一)启动与监听
- Spring Boot 源码(九):Spring Boot 事件监听机制源码分析(上)
- SpringBoot2 | SpringBoot监听器源码分析 | 自定义ApplicationListener(六)
- Laravel 给生产环境添加监听事件 - SQL日志监听
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网站转换率优化之道
[美] Khalid Saleh、[美] Ayat Shukairy / 顾 毅 / 人民邮电出版社 / 2012-4 / 45.00元
内容简介: 怎样才能将访问者转化为顾客? 本书提供了一些切实可行的建议,比如如何说服访问者作出购买决定,如何避免用户因信息过量或导航繁琐而离开网站等。不论你是在设计或营销大型电子商务网站,还是在管理中小型在线业务,都可以从本书学会怎样使用市场营销原则、设计方法、可用性原则和分析数据来持续提升网站的转换率。 作者帮助过众多公司吸引在线顾客,有着丰富的实战经验,在书中细致讨论了从访问......一起来看看 《网站转换率优化之道》 这本书的介绍吧!