这是 react 事件机制的第四节-事件执行,一块儿研究下在这个过程当中主要通过了哪些关键步骤,本文也是react 事件机制的完结篇,但愿本文可让你对 react 事件执行的原理有必定的理解。前端
文章涉及到的源码是基于 react15.6.1版本,虽然不是最新版本可是也不会影响咱们对 react 事件机制的总体把握和理解。react
先简单的回顾下上一文,事件注册的结果是是把全部的事件回调保存到了一个对象中数组
那么在事件触发的过程当中上面这个对象有什么用处呢?浏览器
其实就是用来查找事件回调。函数
按照个人理解,事件触发过程总结为主要下面几个步骤动画
1.进入统一的事件分发函数(dispatchEvent)this
2.结合原生事件找到当前节点对应的ReactDOMComponent对象spa
3.进行事件的合成prototype
3.1根据当前事件类型生成指定的合成对象debug
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的事件。
当我点击child div 的时候,这个时候浏览器会捕获到这个事件,而后通过冒泡,事件被冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。(上一文中咱们已经说过 document 上已经注册了一个统一的事件处理函数 dispatchEvent)
在原生事件对象内已经保留了对应的ReactDOMComponent实例,应该是在挂载阶段就已经保存了
看下ReactDOMComponent实例的内容
事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。
3.1 根据当前事件类型找到对应的合成类,而后进行合成对象的生成
//进行事件合成,根据事件类型得到指定的合成类 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;//最终会返回合成的事件对象 }
3.2 封装原生事件和冒泡机制
在这一步会把原生事件对象挂到合成对象的自身,同时增长事件的默认行为处理和冒泡机制
/** * * @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 代码就明白了
3.3 根据当前节点实例查找他的全部父级实例存入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 长啥样
3.4 在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)
第3步生成完 合成事件对象后,调用栈回到了咱们起初执行的方法内
//在这里执行事件的回调 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 事件触发的过程。
主要流程有:
3.1 根据当前事件类型生成指定的合成对象
3.2 封装原生事件和冒泡机制
3.3 查找当前节点以及他的全部父级
3.4 在listenerBank查找事件回调并合成到 event(事件合成结束)
4.批量处理合成事件内的回调事件(事件触发完成 end)
其中并无深刻到源码的细节,包括事务处理、合成的细节等,另外梳理过程当中本身也有一些疑惑的地方,对源码有兴趣的小伙儿能够深刻研究下,固然仍是但愿本文可以带给你一些启发,若文章有表述不清或有问题的地方欢迎留言交流。
更多精彩内容欢迎关注个人公众号 - 前端张大胖