主要分为4大块儿,主要是结合源码对 react事件机制的原理
进行分析,但愿可让你对 react事件机制有更清晰的认识和理解。javascript
固然确定会存在一些表述不清或者理解不够标准的地方,还请各位大神、大佬斧正。html
01 - 对事件机制的初步理解和验证html5
02 - 对于合成的理解java
03 - 事件注册机制node
04 - 事件执行机制react
01 02 是理论的废话,也算是个人我的总结,没兴趣的能够直接跳到 03-事件执行机制。chrome
ps: 本文基于 react15.6.1进行分析,虽然不是最新版本可是也不会影响咱们对 react 事件机制的总体把握和理解。数组
对 react事件机制
的表象理解,验证,意义和思考。浏览器
先回顾下 对react 事件机制基本理解,react自身实现了一套本身的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。缓存
咱们都知道react 的全部事件并无绑定到具体的dom节点上而是绑定在了document 上,而后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),全部节点的事件都会在 document 上触发。
既然已经有了对 react事件
的一个基本的认知,那这个认知是否正确呢?咱们能够经过简单的方法进行验证。
验证内容:
全部事件均注册到了元素的最顶层-document 上 节点的事件由统一的入口处理 为了方便,直接经过 cli 建立一个项目。
componentDidMount(){ document.getElementById('btn-reactandnative').addEventListener('click', (e) => { console.log('原生+react 事件: 原生事件执行'); }); } handleNativeAndReact = (e) => { console.log('原生+react 事件: 当前执行react事件'); } handleClick=(e)=>{ console.log('button click'); } render(){ return <div className="pageIndex"><p>react event!!!</p <button id="btn-confirm" onClick={this.handleClick}>react 事件</button> <button id="btn-reactandnative" onClick={this.handleNativeAndReact}>原生 + react 事件</button> </div> }
代码中给两个 button
绑定了合成事件,单独给 btn#btn-reactandnative
绑定了一个原生的事件。
而后看下 chrome
控制台,查看元素上注册的事件。
通过简单的验证,能够看到全部的事件根据不一样的事件类型都绑定在了 document
上,触发函数统一是 dispatchEvent
。
若是一个节点上同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样的呢?
其实读到这里答案已经有了。咱们如今基于目前的知识去分析下这个关系。
由于合成事件的触发是基于浏览器的事件机制来实现的,经过冒泡机制冒泡到最顶层元素,而后再由 dispatchEvent
统一去处理。
* 得出的结论:*
原生事件阻止冒泡确定会阻止合成事件的触发。
合成事件的阻止冒泡不会影响原生事件。
为何呢?先回忆下浏览器事件机制
浏览器事件的执行须要通过三个阶段,捕获阶段-目标元素阶段-冒泡阶段。
节点上的原生事件的执行是在目标阶段,然而合成事件的执行是在冒泡阶段,因此原生事件会先合成事件执行,而后再往父节点冒泡。
既然原生都阻止冒泡了,那合成还执行个啥嘞。
好,轮到合成的被阻止冒泡了,那原生会执行吗?固然会了。
由于原生的事件先于合成的执行,因此合成事件内阻止的只是合成的事件冒泡。(代码我就不贴了)
因此得出结论:
原生事件(阻止冒泡)会阻止合成事件的执行
合成事件(阻止冒泡)不会阻止原生事件的执行
二者最好不要混合使用,避免出现一些奇怪的问题
react 本身作这么多的意义是什么?
减小内存消耗,提高性能,不须要注册那么多的事件了,一种事件类型只在 document 上注册一次
统一规范,解决 ie 事件兼容问题,简化事件逻辑
对开发者友好
既然 react 帮咱们作了这么多事儿,那他的背后的机制是什么样的呢?
事件怎么注册的,事件怎么触发的,冒泡机制怎样实现的呢?
请继续日后看......
刚据说合成这个词时候,感受是特别高大上,颇有深度,不是很好理解。
当我大概的了解过react事件机制后,略微了解一些皮毛,我以为合成不仅仅是事件的合成和处理,从广义上来讲还包括:
对原生事件的封装
对某些原生事件的升级和改造
不一样浏览器事件兼容的处理
上面代码是给一个元素添加 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(一个对象)
中
看个最熟悉的代码,也是咱们平常的写法
//此处代码省略 handleFatherClick=()=>{ } handleChildClick=()=>{ } render(){ return <div className="box"> <div className="father" onClick={this.handleFatherClick}> <div className="child" onClick={this.handleChildClick}>child </div> </div> </div> }
通过 babel
编译后,能够看到最终调用的方法是 react.createElement
,z并且声明的事件类型和回调就是个 props
。
react.createElement
执行的结果会返回一个所谓的虚拟 dom (react element object)
ReactDOMComponent
在进行组件加载(mountComponent)、更新(updateComponent)的时候,须要对props进行处理(_updateDOMProperties):
能够看下 registrationNameModules 的内容,就不细说了,他就是一个内置的常量。
接着上面的代码执行到了这个方法
enqueuePutListener(this, propKey, nextProp, transaction);
在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理
根据当前的组件实例获取到最高父级-也就是document,而后执行方法 listenTo
- 也是最关键的一个方法,进行事件绑定处理。
源码文件:ReactBrowerEventEmitter.js
最后执行 EventListener.listen(冒泡)
或者 EventListener.capture(捕获)
,单看下冒泡的注册,其实就是 addEventListener
的第三个参数是 false
。
也能够看到注册事件的时候也对 ie 浏览器作了兼容。
上面没有看到 dispatchEvent 的定义,下面能够看到传入 dispatchEvent 方法的代码。
到这里事件注册就完事儿了。
开始事件的存储,在 react 里全部事件的触发都是经过 dispatchEvent方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下如何存储的 。
react 把全部的事件和事件类型以及react 组件进行关联,把这个关系保存在了一个 map里,也就是一个对象里(键值对),而后在事件触发的时候去根据当前的 组件id和 事件类型查找到对应的 事件fn。
结合源码:
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);//这个方法上面已说完 //这里涉及到了事务,事物会在之后的章节再介绍,主要看事件注册 //下面的代码是将putListener放入数组,当组件挂载完后会依次执行数组的回调。也就是putListener会依次执行 transaction.getReactMountReady().enqueue(putListener, { inst: inst,//组件实例 registrationName: registrationName,//事件类型 click listener: listener //事件回调 fn }); } function putListener() { var listenerToPut = this; //放入数组,回调队列 EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); }
大体的流程就是执行完 listenTo(事件注册),而后执行 putListener 方法进行事件存储,全部的事件都会存储到一个对象中 - listenerBank,具体由 EventPluginHub进行管理。
//拿到组件惟一标识 id var getDictionaryKey = function getDictionaryKey(inst) { return '.' + inst._rootNodeID; } putListener: function putListener(inst, registrationName, listener) { //获得组件 id var key = getDictionaryKey(inst); //获得listenerBank对象中指定事件类型的对象 var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); //存储回调 fn bankForRegistrationName[key] = listener; //.... }
listenerBank其实就是一个二级 map,这样的结构更方便事件的查找。
这里的组件 id 就是组件的惟一标识,而后和fn进行关联,在触发阶段就能够找到相关的事件回调。
看到这个结构是否是很熟悉呢?就是咱们日常使用的 object.
到这里大体的流程已经说完,是否是感受有点明白又不大明白。
不要紧,再来个详细的图,从新理解下。
在事件注册阶段,最终全部的事件和事件类型都会保存到 listenerBank中。
那么在事件触发的过程当中上面这个对象有什么用处呢?
其实就是用来查找事件回调
事件触发过程总结为主要下面几个步骤:
1.进入统一的事件分发函数(dispatchEvent)
2.结合原生事件找到当前节点对应的ReactDOMComponent对象
3.开始 事件的合成
3.1 根据当前事件类型生成指定的合成对象
3.2 封装原生事件和冒泡机制
3.3 查找当前元素以及他全部父级
3.4 在 listenerBank查找事件回调并合成到 event(合成事件结束)
4.批量处理合成事件内的回调事件(事件触发完成 end)
举个栗子
在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子
handleFatherClick=(e)=>{ console.log('father click'); } handleChildClick=(e)=>{ console.log('child click'); } render(){ return <div className="box"> <div className="father" onClick={this.handleFatherClick}> father <div className="child" onClick={this.handleChildClick}>child </div> </div> </div> }
看到这个熟悉的代码,咱们就已经知道了执行结果。
当我点击 child div 的时候,会同时触发father的事件。
进入统一的事件分发函数 (dispatchEvent)。
当我点击child div 的时候,这个时候浏览器会捕获到这个事件,而后通过冒泡,事件被冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。(上一文中咱们已经说过 document 上已经注册了一个统一的事件处理函数 dispatchEvent)。
结合原生事件找到当前节点对应的 ReactDOMComponent对象,在原生事件对象内已经保留了对应的 ReactDOMComponent实例的引用,应该是在挂载阶段就已经保存了。
看下ReactDOMComponent实例的内容
事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。
根据当前事件类型找到对应的合成类,而后进行合成对象的生成
//进行事件合成,根据事件类型得到指定的合成类 var SimpleEventPlugin = { eventTypes: eventTypes, extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; //代码已省略.... var EventConstructor; switch (topLevelType) { //代码已省略.... case 'topClick'://【这里有一个不解的地方】 topLevelType = topClick,执行到这里了,可是这里没有作任何操做 if (nativeEvent.button === 2) { return null; } //代码已省略.... case 'topContextMenu'://而是会执行到这里,获取到鼠标合成类 EventConstructor = SyntheticMouseEvent; break; case 'topAnimationEnd': case 'topAnimationIteration': case 'topAnimationStart': EventConstructor = SyntheticAnimationEvent;//动画类合成事件 break; case 'topWheel': EventConstructor = SyntheticWheelEvent;//鼠标滚轮类合成事件 break; case 'topCopy': case 'topCut': case 'topPaste': EventConstructor = SyntheticClipboardEvent; break; } var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget); EventPropagators.accumulateTwoPhaseDispatches(event); return event;//最终会返回合成的事件对象 }
在这一步会把原生事件对象挂到合成对象的自身,同时增长事件的默认行为处理和冒泡机制
/** * * @param {obj} dispatchConfig 一个配置对象 包含当前的事件依赖 ["topClick"],冒泡和捕获事件对应的名称 bubbled: "onClick",captured: "onClickCapture" * @param {obj} targetInst 组件实例ReactDomComponent * @param {obj} nativeEvent 原生事件对象 * @param {obj} nativeEventTarget 事件源 e.target = div.child */ function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) { this.dispatchConfig = dispatchConfig; this._targetInst = targetInst; this.nativeEvent = nativeEvent;//将原生对象保存到 this.nativeEvent //此处代码略..... var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; //处理事件的默认行为 if (defaultPrevented) { this.isDefaultPrevented = emptyFunction.thatReturnsTrue; } else { this.isDefaultPrevented = emptyFunction.thatReturnsFalse; } //处理事件冒泡 ,thatReturnsFalse 默认返回 false,就是不阻止冒泡 this.isPropagationStopped = emptyFunction.thatReturnsFalse; return this; }
下面是增长的默认行为和冒泡机制的处理方法,其实就是改变了当前合成对象的属性值, 调用了方法后属性值为 true,就会阻止默认行为或者冒泡。
//在合成类原型上增长preventDefault和stopPropagation方法 _assign(SyntheticEvent.prototype, { preventDefault: function preventDefault() { // ....略 this.isDefaultPrevented = emptyFunction.thatReturnsTrue; }, stopPropagation: function stopPropagation() { //....略 this.isPropagationStopped = emptyFunction.thatReturnsTrue; } );
看下 emptyFunction 代码就明白了
根据当前节点实例查找他的全部父级实例存入path
/** * * @param {obj} inst 当前节点实例 * @param {function} fn 处理方法 * @param {obj} arg 合成事件对象 */ function traverseTwoPhase(inst, fn, arg) { var path = [];//存放全部实例 ReactDOMComponent while (inst) { 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);//处理冒泡,从0开始处理,咱们直接看冒泡 } }
看下 path 长啥样
在listenerBank查找事件回调并合成到 event。
紧接着上面代码
fn(path[i], 'bubbled', arg);
上面的代码会调用下面这个方法,在 listenerBank
中查找到事件回调,并存入合成事件对象。
/**EventPropagators.js * 查找事件回调后,把实例和回调保存到合成对象内 * @param {obj} inst 组件实例 * @param {string} phase 事件类型 * @param {obj} event 合成事件对象 */ function accumulateDirectionalDispatches(inst, phase, event) { var listener = listenerAtPhase(inst, event, phase); if (listener) {//若是找到了事件回调,则保存起来 (保存在了合成事件对象内) event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回调进行合并返回一个新数组 event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把组件实例进行合并返回一个新数组 } } /** * EventPropagators.js * 中间调用方法 拿到实例的回调方法 * @param {obj} inst 实例 * @param {obj} event 合成事件对象 * @param {string} propagationPhase 名称,捕获capture仍是冒泡bubbled */ function listenerAtPhase(inst, event, propagationPhase) { var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(inst, registrationName); } /**EventPluginHub.js * 拿到实例的回调方法 * @param {obj} inst 组件实例 * @param {string} registrationName Name of listener (e.g. `onClick`). * @return {?function} 返回回调方法 */ getListener: function getListener(inst, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) { return null; } var key = getDictionaryKey(inst); return bankForRegistrationName && bankForRegistrationName[key]; }
为何可以查找到的呢?
由于 inst (组件实例)里有_rootNodeID,因此也就有了对应关系。
到这里事件合成对象生成完成,全部的事件回调已保存到了合成对象中。
批量处理合成事件对象内的回调方法(事件触发完成 end)。
生成完 合成事件对象后,调用栈回到了咱们起初执行的方法内。
//在这里执行事件的回调 runEventQueueInBatch(events);
到下面这一步中间省略了一些代码,只贴出主要的代码,下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。
贴上最后的执行回调方法的代码
/** * * @param {obj} event 合成事件对象 * @param {boolean} simulated false * @param {fn} listener 事件回调 * @param {obj} inst 组件实例 */ function executeDispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) {//调试环境的值为 false,按说生产环境是 true //方法的内容请往下看 ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { //方法的内容请往下看 ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; } /** ReactErrorUtils.js * @param {String} name of the guard to use for logging or debugging * @param {Function} func The function to invoke * @param {*} a First argument * @param {*} b Second argument */ var caughtError = null; function invokeGuardedCallback(name, func, a) { try { func(a);//直接执行回调方法 } catch (x) { if (caughtError === null) { caughtError = x; } } } var ReactErrorUtils = { invokeGuardedCallback: invokeGuardedCallback, invokeGuardedCallbackWithCatch: invokeGuardedCallback, rethrowCaughtError: function rethrowCaughtError() { if (caughtError) { var error = caughtError; caughtError = null; throw error; } } }; if (process.env.NODE_ENV !== 'production') {//非生产环境会经过自定义事件去触发回调 if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') { 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 经过生成了一个临时节点fakeNode,而后为这个临时元素绑定事件处理程序,而后建立自定义事件 Event,经过fakeNode.dispatchEvent方法来触发事件,而且触发完毕以后当即移除监听事件。
到这里事件回调已经执行完成,可是也有些疑问,为何在非生产环境须要经过自定义事件来执行回调方法。能够看下上面的代码在非生产环境对 ReactErrorUtils.invokeGuardedCallback
方法进行了重写。
主要是从总体流程上介绍了下 react事件的原理,其中并无深刻到源码的各个细节,包括事务处理、合成的细节等,另外梳理过程当中本身也有一些疑惑的地方,感受说原理还能比较容易理解一些,可是一结合源码来写就会以为乱,由于 react代码过于庞大,并且盘根错节,很难抽离,对源码有兴趣的小伙儿能够深刻研究下,固然仍是但愿本文可以带给你一些启发,若文章有表述不清或有问题的地方欢迎留言、 交流、斧正。
https://zhuanlan.zhihu.com/p/35468208
https://react.docschina.org/docs/events.html
回复“加群”与大佬们一块儿交流学习~