本人研究的源代码是0.8.0版本的,可能跟最新版本的事件系统有点出入。javascript
首先,合成事件这个名词是从“Synthetic Event”翻译过来的,在react的官方文档和源码中,这个术语狭义上是指合成事件对象,一个普通的javascript对象。而在这里,咱们谈论的是由众多不一样类型事件的合成事件对象组成的合成事件系统(React’s Event System)。在个人理解里面,合成事件是相对浏览器原生的事件系统而言的。合成事件系统本质上是遵循W3C的相关规范,把浏览器实现过的事件系统再实现一遍,并抹平各个浏览器的实现差别,使得开发者使用起来体验是一致的。html
在开始理解什么是合成事件系统以前,咱们不妨看看我翻译的react的合成事件对象。从这篇文档,咱们能够获得如下关于合成事件系统与原生事件系统异同方面的结论:html5
event target
,current event target
, event object
,event phase
和 propagation path
等核心概念上的定义是一致的。propagation path
上传播。换句话说,就是同一个propagation path
上的每个event listener拿到的event object
都是同一个。也就是说,这二者采用的架构,实现的接口都是一致的。由于二者都遵循W3C的标准规范。java
在这里之因此要提到react合成事件系统与原生系统上的异同点,这是由于我以为带着“形成二者之间的差异的缘由是什么呢?”这个疑问去探索react的合成事件系统会更有针对性。由于源码每每是繁复的,如同茫然而无边际的原始森林通常,一旦咱们没有目标,就容易迷失在这原始森林里,最终一无所得。node
在ReactEventEmitter.js的源码中,官方给出了这样的架构图:react
+------------+ .
| DOM | .
+---^--------+ . +-----------+
| + . +--------+|SimpleEvent|
| | . | |Plugin |
+---|--|------+ . v +-----------+
| | | | . +--------------+ +------------+
| | +-------------->|EventPluginHub| | Event |
| | . | | +-----------+ | Propagators|
| ReactEvent | . | | |TapEvent | |------------|
| Emitter | . | |<---+|Plugin | |other plugin|
| | . | | +-----------+ | utilities |
| +-----------.---------+ | +------------+
| | | . +----|---------+
+-----|------+ . | ^ +-----------+
| . | | |Enter/Leave|
+ . | +-------+|Plugin |
+-------------+ . v +-----------+
| application | . +----------+
|-------------| . | callback |
| | . | registry |
| | . +----------+
+-------------+ .
.
React Core . General Purpose Event Plugin System
复制代码
从官方给出的架构图,咱们能够看到如下主要的角色:程序员
下面咱们来分析一下他们之间的关系。web
注意,原架构图,是没有从ReactEventEmitter到DOM的关系链的,是本人添加的。编程
从ReactEventEmitter指向DOM的关系是指,ReactEventEmitter负责向DOM的顶层进行事件委托。该关系链对应的大致调用栈是(序号越小,表示越先调用):数组
“DOM -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry”这条关系链指的是当用户跟DOM交互触发了原生事件的时候,由于ReactEventEmitter经过createTopLevelCallback方法事先在top level上注册了各类事件的监听器,因此,最早通知到的是ReactEventEmitter。而后才是ReactEventEmitter通知EventPluginHub去找到相应的event plugin,让它去合成此次事件dispatch所须要event object,而后执行dispatch任务。在dispatch任务的执行过程当中,EventPluginHub须要从CallbackRegistry中找到对应的event listener(或者称之为event callback)并调用它。 该关系链对应的大致调用栈是:
“application -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry”这条关系存在于application event listener存储阶段。ReactEventEmitter负责在react component的首次挂载阶段对开发者写的event listener进行收集和存储。在我看来,只须要存在“application -> CallbackRegistry”的关系就好,不知道源码中为何利用引用传递,绕来绕去,把整个关系链延伸得这么长。该关系链对应的大致调用栈是:
“xxxEventPlugin”与EventPluginHub的关系。
各类“xxxEventPlugin”是被注入(inject)到EventPluginHub里面的,换句话说,就是“xxxEventPlugin”的引用会被挂载在EventPluginHub.registrationNames对象的各个key上。注入后的具体数据结构是这样的:
EventPluginHub.registrationNames = {
onBlur: xxxEventPlugin,
onBlurCapture: xxxEventPlugin,
onChange: xxxEventPlugin,
onChangeCapture: xxxEventPlugin,
......
}
复制代码
在v0.8.0的源码中,eventPlugin主要有如下几个:
既然“xxxEventPlugin”是被注入到EventPluginHub里面的,那么咱们不由问,是在哪里被注入的呢?答曰:是在react.js初始化阶段,react根组建没有被初始挂载以前完成的。具体代码在ReactDefaultInjection.js里面:
/**
* Some important event plugins included by default (without having to require
* them).
*/
EventPluginHub.injection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
CompositionEventPlugin: CompositionEventPlugin,
MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
SelectEventPlugin: SelectEventPlugin
});
复制代码
从集合的概念上讲,EventPluginHub与eventPlugin的关系是“一对多”的关系。全部的eventPlugin都要注入到EventPluginHub中去。
“xxxEventPlugin”与“SyntheticxxxEvent”的关系
EventPlugin在合成event object的时候,不一样类型的事件,须要调用不一样的合成事件的构造函数,也就是说,“xxxEventPlugin”与“SyntheticxxxEvent”造成了一对多的关系。咱们拿SimpleEventPlugin的extractEvents方法作个示例:
/** * @param {string} topLevelType Record from `EventConstants`. * @param {DOMEventTarget} topLevelTarget The listening component root node. * @param {string} topLevelTargetID ID of `topLevelTarget`. * @param {object} nativeEvent Native browser event. * @return {*} An accumulation of synthetic events. * @see {EventPluginHub.extractEvents} */ extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { return null; } var EventConstructor; switch(topLevelType) { case topLevelTypes.topInput: case topLevelTypes.topSubmit: // HTML Events // @see http://www.w3.org/TR/html5/index.html#events-0 EventConstructor = SyntheticEvent; break; case topLevelTypes.topKeyDown: case topLevelTypes.topKeyPress: case topLevelTypes.topKeyUp: EventConstructor = SyntheticKeyboardEvent; break; case topLevelTypes.topBlur: case topLevelTypes.topFocus: EventConstructor = SyntheticFocusEvent; break; case topLevelTypes.topClick: // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return null; } /* falls through */ case topLevelTypes.topContextMenu: case topLevelTypes.topDoubleClick: case topLevelTypes.topDrag: case topLevelTypes.topDragEnd: case topLevelTypes.topDragEnter: case topLevelTypes.topDragExit: case topLevelTypes.topDragLeave: case topLevelTypes.topDragOver: case topLevelTypes.topDragStart: case topLevelTypes.topDrop: case topLevelTypes.topMouseDown: case topLevelTypes.topMouseMove: case topLevelTypes.topMouseUp: EventConstructor = SyntheticMouseEvent; break; case topLevelTypes.topTouchCancel: case topLevelTypes.topTouchEnd: case topLevelTypes.topTouchMove: case topLevelTypes.topTouchStart: EventConstructor = SyntheticTouchEvent; break; case topLevelTypes.topScroll: EventConstructor = SyntheticUIEvent; break; case topLevelTypes.topWheel: EventConstructor = SyntheticWheelEvent; break; case topLevelTypes.topCopy: case topLevelTypes.topCut: case topLevelTypes.topPaste: EventConstructor = SyntheticClipboardEvent; break; } ("production" !== process.env.NODE_ENV ? invariant( EventConstructor, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType ) : invariant(EventConstructor)); var event = EventConstructor.getPooled( dispatchConfig, topLevelTargetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 复制代码
从上面的源码能够看到,extractEvents方法会根据不一样的事件类型,使用不一样的“SyntheticxxxEvent”构造函数来构造合成事件对象。SimpleEventPlugin用到的构造函数有如下:
EventPluginHub,eventPlugin和SyntheticEvent三者之间的关系以下:
在对每一个阶段进行分析前,我先作个预告。预告一下几种数据结构和两种关系。
listenerBank = { onClick: { '[0].[1]': listener // listener就是咱们挂载在jsx的事件回调 }, onClickCapture: { '[0].[1]': listener } } 复制代码
正如上面我提到的,阅读源码必须有一个聚焦的目标。咱们不妨从注册在react component上的event listener的身上出发,探究一下,从咱们注册开始,到event listener被调用的这个过程,咱们的event listener到底经历了什么?通过研究整理,咱们能够把event listener这个生命周期划分为四个阶段:
准备阶段
1.1. 各类eventPlugin的提早注入(依赖注入)
1.2. 提早在document上对全部已支持的事件进行监听(事件委托)
存储阶段
2.1. 找到收集入口
2.2. 储存application event listener
调用阶段
3.1. 根据eventType去查找eventPlugin
3.2. 组建eventQueue(由组建不一样类型事件的events组成)
第一步:构造SyntheticEvent的实例;
第二步:往SyntheticEvent的实例添加各类加强属性和用于抹平跨浏览器差别的兼容属性;
第三步: 分别沿着event target的捕获阶段传播路径和冒泡阶段传播路劲去取回application event listener,并按照先捕获,后冒泡的顺序推入到存放listener的队列中,也就是event._dispatchListeners。
3.3. 循环eventQueue,依次dispatch每个SyntheticEvent
收尾阶段
主要是对eventQueue作垃圾回收,释放SyntheticEvent实例,让它从新回到pooling池中。
准备阶段作了两件事:
由于react的开发者们很早就考虑到react要应用到跨平台开发中,因此,他们很早就着手分离react的核心代码和平台相关的的代码了。早期采用的依赖注入模式和后期采用的分包模式,就是他们进行跨平台架构所采用的主要手段。在react合成事件系统中,对EventPluginHub的实现就是采用了这种依赖注入模式组织而成的。在react.js程序入口出,咱们对EventPluginHub的依赖进行了注入(见ReactDefaultInjection.js开头):
function inject() { ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback; /** * Inject module for resolving DOM hierarchy and plugin ordering. */ EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder); EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); /** * Some important event plugins included by default (without having to require * them). */ EventPluginHub.injection.injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, CompositionEventPlugin: CompositionEventPlugin, MobileSafariClickEventPlugin: MobileSafariClickEventPlugin, SelectEventPlugin: SelectEventPlugin }); // ......other code here } 复制代码
从代码中,咱们能够看出,咱们往EventPluginHub里面注入了EventPluginOrder,InstanceHandle和本平台(web)所须要用到的全部eventPlugin。eventPlugin是用来干吗的,这里就不重复解释了,上面说过。咱们在里说说剩余的两个:EventPluginOrder和InstanceHandle。
咱们直译就是“事件插件顺序”的意思。其实,更确切地说,应该是指“事件插件的加载顺序”。这个被注入的顺序是怎么的呢?见源码DefaultEventPluginOrder.js:
/** * Module that is injectable into `EventPluginHub`, that specifies a * deterministic ordering of `EventPlugin`s. A convenient way to reason about * plugins, without having to package every one of them. This is better than * having plugins be ordered in the same order that they are injected because * that ordering would be influenced by the packaging order. * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that * preventing default on events is convenient in `SimpleEventPlugin` handlers. */ var DefaultEventPluginOrder = [ keyOf({ResponderEventPlugin: null}), keyOf({SimpleEventPlugin: null}), keyOf({TapEventPlugin: null}), keyOf({EnterLeaveEventPlugin: null}), keyOf({ChangeEventPlugin: null}), keyOf({SelectEventPlugin: null}), keyOf({CompositionEventPlugin: null}), keyOf({AnalyticsEventPlugin: null}), keyOf({MobileSafariClickEventPlugin: null}) ]; 复制代码
转换一下,DefaultEventPluginOrder最后的值是这样的:
var DefaultEventPluginOrder = [ 'ResponderEventPlugin', 'SimpleEventPlugin', 'TapEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'CompositionEventPlugin', 'AnalyticsEventPlugin', 'MobileSafariClickEventPlugin' ]; 复制代码
也就是说,咱们须要eventPlugin按照上面的顺序加载并执行。为何须要规定eventPlugin按照必定的顺序加载,执行呢?从源码的注释上咱们不难找到问题的答案。那就是:某些plugin须要先于某些plugin加载并执行。好比ResponderEventPlugin就必须在SimpleEventPlugin加载以前加载,不然SimpleEventPlugin负责处理的event listenter里面就没法阻止事件的默认行为。由于每一次打包的顺序是没办法保证100%都是一致的,因此手动地按照顺序引入每个plugin,手动地按照顺序注入每个plugin的这种方案也是不太可靠的。相比之下,显示地声明一个plugin的加载顺序,而后手动地调用publishRegistrationName方法来加载plugin,这种方案更好。
从上面给出的源代码:
EventPluginHub.injection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
CompositionEventPlugin: CompositionEventPlugin,
MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
SelectEventPlugin: SelectEventPlugin
});
复制代码
咱们能够看出,咱们总共用到了SimpleEventPlugin,EnterLeaveEventPlugin,ChangeEventPlugin,CompositionEventPlugin,MobileSafariClickEventPlugin和SelectEventPlugin这六个plugin。而它们加载的顺序就是咱们上面给出的顺序:
var DefaultEventPluginOrder = [ 'ResponderEventPlugin', 'SimpleEventPlugin', 'TapEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'CompositionEventPlugin', 'AnalyticsEventPlugin', 'MobileSafariClickEventPlugin' ]; 复制代码
InstanceHandle是一个utils模块。它主要包含着一些用于处理react instance方面需求的工具函数。好比:createReactRootID,getReactRootIDFromNodeID,traverseTwoPhase等等。其中,traverseTwoPhase跟react的合成事件系统关系最为紧密。它将会被用到咱们上面所提到的第三阶段。它主要负责,给定一个reactID,它能从这个reactID所对应的节点出发,沿着捕获路径和冒泡路径去查找并收集注册到当前事件的event listener,并将它们按照正确的顺序入队到存放event listener的队列里面去。这部分的细节,咱们将会在第三阶段那里详细地阐述。
在javascript中,依赖注入的本质是引用传递。所以,咱们能够说js的依赖注入是隐式的引用传递。纵观EventPluginHub的内部代码,你会发现,里面存在大量的引用传递。EventPluginHub
这个模块,就像一个甩手掌柜同样,其实啥大事也没有干,它都把它的大部分工做交给了CallbackRegistry
和EventPluginRegistry
。这个场景让我想起了一个对于中国程序员来讲甚是美丽而悲伤的“故事”。在这个故事里面,Bob跟EventPluginHub
同样,没作太多事情,却躺赢了人生。
在第一阶段,除了对EventPluginHub进行了依赖注入,还对ReactEventEmitter也进行了依赖注入。
ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback;
复制代码
注入的ReactEventTopLevelCallback方法用于建立绑定在top level上event listener。咱们会在下面的事件委托部分进行详细的阐述。
事件委托模式已是咱们的老朋友了,在jQuery时代,咱们早就接触过了。事件委托原理的核心要素是原生事件的“事件冒泡”机制和“event target”。事件委托的总体流程大致以下:
此时,咱们基本上能够看清“委托(delegation)”的含义。若是说“调用event listener”是一项须要完成的事情的话,那么相比于咱们本身来作(直接在原生DOM元素上监听,等待浏览器直接调用咱们的event listener),咱们如今把这件事“委托”给了这个原生DOM元素的祖先元素,让它在它的事件回调被浏览器调用时,间接地来调用咱们的event listenter。
也许你会问:“原生DOM元素的祖先元素为何会有它的事件回调呢?”。
答:“固然是须要咱们(指的是像jQuery和react这样的类库)事先手动地作事件监听啦”;
也许你又会问:“当用户点击原生DOM元素的时候,为何它的祖先元素的事件回调会执行呢?”。
答:“由于有“事件冒泡”这一机制在”;
也许你还会问:“全部元素都将事件监听委托给同一个祖先元素,那么当事件触发时,该祖先元素是怎么知道该调用哪些event listener呢?”。
答:根据原生的event对象的target属性,咱们能够先肯定事件传播的路径,再收集该路径上全部元素的绑定的event listener便可。
不管在jQuery中,仍是react中,事件委托的运行流程大抵跟上面提到的差很少。 咱们此处要说的其实是指这四个流程里面的第一步。也就是react合成事件系统中所说的“listen at top level”。
源码上是用了“top level”这个术语,一番源码查阅下来,它其实就是指“document对象”。下面看源码(在ReactMount.js里面):
prepareEnvironmentForDOM: function(container) { ("production" !== process.env.NODE_ENV ? invariant( container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ), 'prepareEnvironmentForDOM(...): Target container is not a DOM element.' ) : invariant(container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ))); // 注意:document.documentElement的nodeType也是1 // 此处是为了获取文档对象:document var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container; ReactEventEmitter.ensureListening(ReactMount.useTouchEvents, doc); } 复制代码
正如上面注释所说,这个doc变量的值最终是document对象。若是你往调用栈追溯下去的话:
你会发现,咱们的doc会被传入到一个叫listen的方法里面:
到这里,咱们也看到了熟悉的原生方法“addEventListener”了,咱们也就能够肯定,这个所谓的“top level”就是document对象了。
好,既然咱们肯定了“top level”就是document对象了。那么接下来就是探究一下如何“listen at”了。
我也不卖关子了,其实react的“listen at”就是枚举式地,一个个地在document对象上,对目前全部的浏览器事件作了事件监听。直接上源代码(在ReactEventEmitter.js里面):
listenAtTopLevel: function(touchNotMouse, contentDocument) { ("production" !== process.env.NODE_ENV ? invariant( !contentDocument._isListening, 'listenAtTopLevel(...): Cannot setup top-level listener more than once.' ) : invariant(!contentDocument._isListening)); var topLevelTypes = EventConstants.topLevelTypes; var mountAt = contentDocument; registerScrollValueMonitoring(); trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt); trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt); trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt); trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt); trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt); trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt); trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt); trapBubbledEvent(topLevelTypes.topContextMenu, 'contextmenu', mountAt); if (touchNotMouse) { trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt); trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt); trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt); trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt); } trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt); trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt); trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt); trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt); trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt); trapBubbledEvent( topLevelTypes.topSelectionChange, 'selectionchange', mountAt ); trapBubbledEvent( topLevelTypes.topCompositionEnd, 'compositionend', mountAt ); trapBubbledEvent( topLevelTypes.topCompositionStart, 'compositionstart', mountAt ); trapBubbledEvent( topLevelTypes.topCompositionUpdate, 'compositionupdate', mountAt ); if (isEventSupported('drag')) { trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt); trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt); trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt); trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt); trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt); trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt); trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt); trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt); } if (isEventSupported('wheel')) { trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); } else if (isEventSupported('mousewheel')) { trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); } else { // Firefox needs to capture a different mouse scroll event. // @see http://www.quirksmode.org/dom/events/tests/scroll.html trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); } // IE<9 does not support capturing so just trap the bubbled event there. if (isEventSupported('scroll', true)) { trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); } else { trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); } if (isEventSupported('focus', true)) { trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); } else if (isEventSupported('focusin')) { // IE has `focusin` and `focusout` events which bubble. // @see // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); } if (isEventSupported('copy')) { trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt); trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt); trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt); } } 复制代码
从上面的代码中,咱们能够看到,react几乎对咱们所熟知的事件分别在冒泡阶段和捕获阶段(如何支持的话)做了事件监听。topLevelType的事件名就是将原生的事件名改成小驼峰的写法,而且在前面加上“top”前缀。为了直观,我在createTopLevelCallback方法中把全部的topLevelType答应出来看看:
一番探索下来,react合成事件系统中的“listen at top level”其实也没有想象中的那么高深,现在看起来,甚至有些笨拙。由于react合成事件系统是采用了事件委托模式,而且topLevelType是注册在事件的冒泡阶段,因此咱们能够得出如下结论:
下面,我以click事件为例,验证一下结论1:
// 在应用代码中打log handleClick=()=> {console.log('btn react click event')}} handleClickCapture=()=> {console.log('btn react clickCapture event')}} render() { return ( <button id="btn" onClick={this.handleClick} onClickCapture={this.handleClickCapture} > 点我试一试 </button> ) } // 在源码中打log createTopLevelCallback: function createTopLevelCallback(topLevelType) { return function (nativeEvent) { if (nativeEvent.type === 'click') { console.log('document native click callback'); } if (!_topLevelListenersEnabled) { return; } // TODO: Remove when synthetic events are ready, this is for IE<9. if (nativeEvent.srcElement && nativeEvent.srcElement !== nativeEvent.target) { nativeEvent.target = nativeEvent.srcElement; } var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(nativeEvent)) || window; var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; ReactEventEmitter.handleTopLevel(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent); }; } 复制代码
最后打印出来的结果是:
document native click callback
btn react clickCapture event
btn react click event
复制代码
从而验证告终论1是正确的。
下面,咱们再来验证一下结论2:
在进行验证以前,咱们要明白这么一个现象:“用户一个交互动做,可能会触发多个事件”。好比,“点击按钮”的这么一个交互动做,对于了按钮元素来讲,就有可能触发“mousedown”, “mouseup”, “click”。在react的合成事件系统里面,还会多个“focus”事件。
handleMousedown=()=> {console.log('react mousedown event')} handleMouseup=()=> {console.log('react mouseup event')} handleClick=()=> {console.log('react click event')}} render() { return ( <button id="btn" onMouseDown={this.handleMousedown} onMouseUp={this.handleMouseup} onClick={this.handleClick} > 点我试一试 </button> ) } componentDidMount(){ const btn = doucument.getElementById('btn'); btn.addEventListener('mousedown',()=> { console.log('native mousedown event')}); btn.addEventListener('mouseup',()=> { console.log('native mouseup event')}); btn.addEventListener('click',()=> { console.log('native click event')}); } 复制代码
打印结果以下:
native mousedown event
react mousedown event
native mouseup event
react mouseup event
native click event
react click event
复制代码
那么你会发现react的event listener的调用顺序跟原生的event listener的调用顺序是一致的,从而验证告终论2是正确的。
由于事件委托模式的运行有赖于浏览器原生的事件冒泡机制,那咱们不由问,假如咱们在某个事件的冒泡路径上阻止了事件传播,那么react的event listener是否是就不会执行啦?咱们不妨使用下面代码来验证一下:
handleClick=()=> {console.log('react click event')}} render() { return ( <button id="btn" onMouseDown={this.handleMousedown} onMouseUp={this.handleMouseup} onClick={this.handleClick} > 点我试一试 </button> ) } componentDidMount(){ const btn = doucument.getElementById('btn'); btn.addEventListener('click',(event)=> { event.stopPropragation(); }); } 复制代码
以上代码执行后,你会发现,点击button,react的event listener就不执行了。这是由于注册在document对象上的topLevelCallback并无执行。若是咱们把event.stopPropragation()
语句注释了,那么控制台就会从新打印出react click event
。这从而证实了事件委托模式的坑仍是有点深的:若是你在开发过程当中,原生事件监听与react事件监听混用,一不当心写出这种代码的话,那么你这个button事件传播路径上的全部的react event listener都不会执行了。
至此,react合成系统运行的第一阶段已经讲解完毕了,下面咱们进入第二阶段的讲解。
用户(也就是开发者)注册的event listener通常称之为“application event listener”,下面,咱们简称为“event listener”。而存储阶段就是指从react element身上收集,并存储在事件监听登记表上的过程。
首先,看看在react中,咱们是怎样地注册事件监听的。若是是写成jsx的话,那么是这样的:
handleClick=()=> {console.log('react click event')}} render() { return ( <button id="btn" onClick={this.handleClick} > 点我试一试 </button> ) } 复制代码
假如咱们换成js的写法,更能看透react事件监听的原生面貌:
handleClick=()=> {console.log('react click event')}} render() { return React.DOM.button({ id: 'btn', onClick: this.handleClick }, '点我试一试'); } 复制代码
jsx写法很像DOM1的事件监听,若是咱们不假思索,很容易被感受所迷惑。觉得本身在写着一些原生的事件监听的代码。其实否则。说到底,react的事件监听写法本质就是对象里面的key-value对。key是“onClick”,value是event listener的函数引用。把函数当成值来使用,是javascript编程的一大特点。所以,我嗯能够在不看源码的前提下,推测这个event listener函数会被某个第三方收集暂存起来。
由于“onClick”是react element的一个prop,而这种事件监听的prop只有写在reactDOMComponent身上才有用,因此咱们不妨去reactDOMComponent相关的代码里面看看。左瞧右瞧,咱们在reactDOMComponent.js的_createOpenTagMarkup方法里面看到这样的一行代码:
_createOpenTagMarkup: function() { // ...... if (registrationNames[propKey]) { putListener(this._rootNodeID, propKey, propValue); } // ..... 复制代码
registrationNames是一个怎样的存在呢?通过追溯,咱们发现它就是当前浏览器所支持的事件名改成小驼峰后,再加上“on”为前缀的事件名的集合。打印出来,是这样的:
好,咱们明白了收集的时机,接下来,咱们就是要弄清楚,react是如何收集的问题了。在一番代码导航的操做下,咱们最终到达了咱们的目的地:CallbackRegistry.js的putListener方法:
/* * @param {string} id ID of the DOM element. * @param {string} registrationName Name of listener (e.g. `onClick`). * @param {?function} listener The callback to store. */ putListener: function(id, registrationName, listener) { var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[id] = listener; } 复制代码
慢着,listenerBank这个变量是怎么回事呢?眼光往上移动,咱们会看到:
var listenerBank = {};
复制代码
对,这是CallbackRegistry模块内的全局变量。它就是react帮咱们储存event listener的地方。源码注释也说得很清楚:
/** * Stores "listeners" by `registrationName`/`id`. There should be at most one * "listener" per `registrationName`/`id` in the `listenerBank`. * * Access listeners via `listenerBank[registrationName][id]`. */ 复制代码
说得如此直白,我在这里也不啰里八嗦了。存储event listener后的listenerBank的数据结构是形如这样的:
listenerBank = { onClick: { '[0].[1]': listener // listener就是咱们挂载在jsx的事件回调 } 复制代码
为了加深对listenerBank数据结构的印象,咱们把实际应用中listenerBank打印出来的:
相似于'[0].[1]'这种字符串是一个reactid值(在后期版本中,reactid会被去掉?),对应着页面上一个由react渲染出来的真实DOM元素。
通过上面的一些细节分析,咱们能够把event listener的收集过程总结以下:
到这里,react合成事件系统的第二阶段算是讲完了,咱们只须要记住listenerBank对象的数据结构就好,以便于在第三阶段讲解涉及取回event listener时能有很好的理解。
其实,相比event listener的调用阶段(也就是第三阶段),上面提到的一,二阶段均可以算做准备工做。由于,到目前为止,咱们的event listener还乖乖地躺在listenerBank的怀抱里面沉睡呢。
重头戏终于来了。从event listener的函数签名void func(event)
能够得知咱们第四阶段有如下的两个探索点:
事实上,调用阶段的入口函数handleTopLevel正是干了这两件事情:
/** * Streams a fired top-level event to `EventPluginHub` where plugins have the * opportunity to create `ReactEvent`s to be dispatched. * * @param {string} topLevelType Record from `EventConstants`. * @param {object} topLevelTarget The listening component root node. // 这个节点其实就是你点击的那个元素,即event target。 * @param {string} topLevelTargetID ID of `topLevelTarget`. * @param {object} nativeEvent Native environment event. */ handleTopLevel: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { // 1. 合成event object var events = EventPluginHub.extractEvents( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent ); // 2. 调用event listenter ReactUpdates.batchedUpdates(runEventQueueInBatch, events); } 复制代码
其中“合成event object”又分两步走:
全部的eventPlugin核心实现的就是上面两个功能需求。咱们不妨抽取两三个plugin来看看。
extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { return null; } var EventConstructor; switch(topLevelType) { case topLevelTypes.topInput: case topLevelTypes.topSubmit: // HTML Events // @see http://www.w3.org/TR/html5/index.html#events-0 EventConstructor = SyntheticEvent; break; case topLevelTypes.topKeyDown: case topLevelTypes.topKeyPress: case topLevelTypes.topKeyUp: EventConstructor = SyntheticKeyboardEvent; break; case topLevelTypes.topBlur: case topLevelTypes.topFocus: EventConstructor = SyntheticFocusEvent; break; case topLevelTypes.topClick: // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return null; } /* falls through */ case topLevelTypes.topContextMenu: case topLevelTypes.topDoubleClick: case topLevelTypes.topDrag: case topLevelTypes.topDragEnd: case topLevelTypes.topDragEnter: case topLevelTypes.topDragExit: case topLevelTypes.topDragLeave: case topLevelTypes.topDragOver: case topLevelTypes.topDragStart: case topLevelTypes.topDrop: case topLevelTypes.topMouseDown: case topLevelTypes.topMouseMove: case topLevelTypes.topMouseUp: EventConstructor = SyntheticMouseEvent; break; case topLevelTypes.topTouchCancel: case topLevelTypes.topTouchEnd: case topLevelTypes.topTouchMove: case topLevelTypes.topTouchStart: EventConstructor = SyntheticTouchEvent; break; case topLevelTypes.topScroll: EventConstructor = SyntheticUIEvent; break; case topLevelTypes.topWheel: EventConstructor = SyntheticWheelEvent; break; case topLevelTypes.topCopy: case topLevelTypes.topCut: case topLevelTypes.topPaste: EventConstructor = SyntheticClipboardEvent; break; } ("production" !== process.env.NODE_ENV ? invariant( EventConstructor, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType ) : invariant(EventConstructor)); var event = EventConstructor.getPooled( dispatchConfig, topLevelTargetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 复制代码
咱们把目光放在倒数三行代码便可:
var event = EventConstructor.getPooled( dispatchConfig, topLevelTargetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; 复制代码
如上,在extractEvents方法中,被省略掉的代码总的来讲就干了这么一件事:根据topLevelTypes的值,计算出对应的合成事件对象的构造函数。接下来,如咱们所见,EventConstructor.getPooled()
调用返回一个实例--合成事件对象。实例化没有使用new 操做符而是普通的函数调用?这是由于这里使用对象复用(pooling)的技术。关于pooling,上面提到过,其中的技术细节就不展开说了。而倒数第二行的一个函数调用:EventPropagators.accumulateTwoPhaseDispatches(event);
就是要完成第二步骤要作的事情。这个函数调用会产生一个较短的函数调用栈,以下:
getListener() listenerAtPhase() accumulateDirectionalDispatches() traverseParentPath() traverseTwoPhase() accumulateTwoPhaseDispatchesSingle() forEachAccumulated() accumulateTwoPhaseDispatches() 复制代码
这个调用栈是“合成event object”的关键部分,等咱们抽样观察完剩下的eventPlugin再回过头来好好分析。下面,咱们继续抽样。
extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { switch (topLevelType) { // Track the input node that has focus. case topLevelTypes.topFocus: if (isTextInputElement(topLevelTarget) || topLevelTarget.contentEditable === 'true') { activeElement = topLevelTarget; activeElementID = topLevelTargetID; lastSelection = null; } break; case topLevelTypes.topBlur: activeElement = null; activeElementID = null; lastSelection = null; break; // Do not fire the event while the user is dragging. This matches the // semantics of the native select event. case topLevelTypes.topMouseDown: mouseDown = true; break; case topLevelTypes.topContextMenu: case topLevelTypes.topMouseUp: mouseDown = false; return constructSelectEvent(nativeEvent); // Chrome and IE fire non-standard event when selection is changed (and // sometimes when it has not). case topLevelTypes.topSelectionChange: return constructSelectEvent(nativeEvent); // Firefox does not support selectionchange, so check selection status // after each key entry. case topLevelTypes.topKeyDown: if (!useSelectionChange) { activeNativeEvent = nativeEvent; setTimeout(dispatchDeferredSelectEvent, 0); } break; } } 复制代码
而constructSelectEvent的实现是这样的:
function constructSelectEvent(nativeEvent) { // Ensure we have the right element, and that the user is not dragging a // selection (this matches native `select` event behavior). if (mouseDown || activeElement != getActiveElement()) { return; } // Only fire when selection has actually changed. var currentSelection = getSelection(activeElement); if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) { lastSelection = currentSelection; var syntheticEvent = SyntheticEvent.getPooled( eventTypes.select, activeElementID, nativeEvent ); syntheticEvent.type = 'select'; syntheticEvent.target = activeElement; EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent); return syntheticEvent; } } 复制代码
仔细看,咱们又看到一个相同的代码“范式”了:
var syntheticEvent = SyntheticEvent.getPooled( eventTypes.select, activeElementID, nativeEvent ); syntheticEvent.type = 'select'; syntheticEvent.target = activeElement; EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent); return syntheticEvent; 复制代码
嗯嗯,就是SyntheticEvent.getPooled()
和EventPropagators.accumulateTwoPhaseDispatches()
;
extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { var getTargetIDFunc, handleEventFunc; if (shouldUseChangeEvent(topLevelTarget)) { if (doesChangeEventBubble) { getTargetIDFunc = getTargetIDForChangeEvent; } else { handleEventFunc = handleEventsForChangeEventIE8; } } else if (isTextInputElement(topLevelTarget)) { if (isInputEventSupported) { getTargetIDFunc = getTargetIDForInputEvent; } else { getTargetIDFunc = getTargetIDForInputEventIE; handleEventFunc = handleEventsForInputEventIE; } } else if (shouldUseClickEvent(topLevelTarget)) { getTargetIDFunc = getTargetIDForClickEvent; } if (getTargetIDFunc) { var targetID = getTargetIDFunc( topLevelType, topLevelTarget, topLevelTargetID ); if (targetID) { var event = SyntheticEvent.getPooled( eventTypes.change, targetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } } if (handleEventFunc) { handleEventFunc( topLevelType, topLevelTarget, topLevelTargetID ); } } 复制代码
是的,again:
if (targetID) { var event = SyntheticEvent.getPooled( eventTypes.change, targetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 复制代码
抽样完毕。正如咱们上面所下的结论那样,全部的eventPlugin所要实现的核心两个功能需求就是:
由于,实例化合成事件对象这个过程包含了不少实现细节,好比应对浏览器差别而作的兼容细节,pooling技术等等。同时,不一样的事件类型所要作的浏览器兼容不尽相同,不一样的事件类型的构造函数实现方式也不尽相同。这里里面包含太多的细节了,跟咱们的主线没有太密切的关系,故不深刻探究了。感兴趣的同窗,可另行研究。这里把重点放在第二步将要调用的event listener保存在这个对象上。说白一点,就是说,咱们要研究的就是上面在分析SimpleEventPlugin时所提到函数调用栈道:
getListener() // 栈顶 listenerAtPhase() accumulateDirectionalDispatches() traverseParentPath() traverseTwoPhase() accumulateTwoPhaseDispatchesSingle() forEachAccumulated() accumulateTwoPhaseDispatches() // 栈底 复制代码
从这个调用栈顶部的getListener()方法名得知,咱们的研究方向是没错了。由于不管如具体实现如何,去收集event listener的这个动做都应该发生的。那么,接下来,咱们带着“调用阶段,event listener的收集过程是如何进行的呢?”这个疑问继续探索下去。
首先,咱们看看accumulateTwoPhaseDispatches函数的签名:void func(events)
。从函数签名,咱们能够看到参数叫events。从调试结果来看,这个events的数据类型能够是单个event object,也能够是多个event object组成的数组。大多数状况下,咱们看到都是单个event object。而什么状况下是数组呢?这个目前我还没研究出来,择日研究吧。从accumulateTwoPhaseDispatches()这个方法名,咱们能够得知,这个过程就是在各个传入的event object身上去累积(accumulate)event listenter的过程。又由于在这里,咱们讨论的events实参是单个object的状况,因此,forEachAccumulated()方法就形同虚设了。为何这么说呢?看它的代码是实现就知道:
/** * @param {array} an "accumulation" of items which is either an Array or * a single item. Useful when paired with the `accumulate` module. This is a * simple utility that allows us to reason about a collection of items, but * handling the case when there is exactly one item (and we do not need to * allocate an array). */ var forEachAccumulated = function forEachAccumulated(arr, cb, scope) { if (Array.isArray(arr)) { arr.forEach(cb, scope); } else if (arr) { cb.call(scope, arr); } }; 复制代码
在咱们讨论的状况中,最终代码会执行到else if
分支。也就是会说,最终结果会来到accumulateTwoPhaseDispatchesSingle(event)这个方法调用:
/** * Collect dispatches (must be entirely collected before dispatching - see unit * tests). Lazily allocate the array to conserve memory. We must loop through * each event and perform the traversal for each one. We can not perform a * single traversal for the entire collection of events because each event may * have a different target. * 方法名中的“single”指的是每个event object */ function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { injection.InstanceHandle.traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event); } } 复制代码
所谓的“累积event listener(accumulated dispatches)”说白一点就是在实例化后的event object身上开辟了两个字段:“_dispatchIDs” 和 “_dispatchListeners”,分别用于保存须要被分发event object的DOM节点的reactId和它上面所注册的event listener。这个累积过程就是从触发事件的event target开始,遍历它的捕获阶段和冒泡阶段,去收集相关的reactId和event listener。这就是方法名中的“two phase”的意思了。至于方法名中的“single”指的events里面的“each single event object”了。注意,event object里面有个dispatchMarker字段,这个字段就是event target身上的reactId。
接下来的,进入的traverseTwoPhase(targetID, cb, arg)方法负责的正是真真正正的遍历:
/** * Simulates the traversal of a two-phase, capture/bubble event dispatch. * * NOTE: This traversal happens on IDs without touching the DOM. * * @param {string} targetID ID of the target node. * @param {function} cb Callback to invoke. * @param {*} arg Argument to invoke the callback with. * @internal */ traverseTwoPhase: function traverseTwoPhase(targetID, cb, arg) { // console.log('targetID:', targetID); if (targetID) { traverseParentPath('', targetID, cb, arg, true, false); traverseParentPath(targetID, '', cb, arg, false, true); } }, 复制代码
若是往前去探究traverseParentPath方法的签名void function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast)
,咱们就是发现,if条件分支里的第一行语句traverseParentPath('', targetID, cb, arg, true, false);
指的就是遍历event target事件传播的捕获阶段;而第二行语句traverseParentPath(targetID, '', cb, arg, false, true); }
则是指遍历event target事件传播的冒泡阶段(传参的那个空的字符串表明这event target层级关系中最远的祖先元素的父节点)。注意,一个完整的事件传播中,先进行捕获阶段,再进行冒泡阶段,这二者的前后顺序就是由这两行代码的前后顺序所决定的。不信?那咱们就来验证如下。咱们在同一个元素上同时注册了冒泡事件和捕获事件,在event listener里面打个log,结果以下:
如你所见,这个顺序已经改变了。这也证实了个人结论是正确的了。好了,接下来轮到 traverseParentPath方法来作切实的for循环。咱们来看看它的源码:
/** * Traverses the parent path between two IDs (either up or down). The IDs must * not be the same, and there must exist a parent path between them. * * @param {?string} start ID at which to start traversal. * @param {?string} stop ID at which to end traversal. * @param {function} cb Callback to invoke each ID with. * @param {?boolean} skipFirst Whether or not to skip the first node. * @param {?boolean} skipLast Whether or not to skip the last node. * @private */ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) { start = start || ''; stop = stop || ''; "production" !== process.env.NODE_ENV ? invariant(start !== stop, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : invariant(start !== stop); var traverseUp = isAncestorIDOf(stop, start); "production" !== process.env.NODE_ENV ? invariant(traverseUp || isAncestorIDOf(start, stop), 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(traverseUp || isAncestorIDOf(start, stop)); // Traverse from `start` to `stop` one depth at a time. var depth = 0; var traverse = traverseUp ? getParentID : getNextDescendantID; for (var id = start;; /* until break */id = traverse(id, stop)) { if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { cb(id, traverseUp, arg); } if (id === stop) { // Only break //after// visiting `stop`. break; } "production" !== process.env.NODE_ENV ? invariant(depth++ < MAX_TREE_DEPTH, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop) : invariant(depth++ < MAX_TREE_DEPTH); } } 复制代码
看到for循环了吗?对,就是在for循环里面,一个个地把event listener入队到event._dispatchListeners数组里面的。for循环里面的第一个if其实能够转换为:
if(!((skipFirst && id === start) || (skipLast && id === stop))) { cb(id, traverseUp, arg); } 复制代码
也便是除了传播路径上 最远祖先元素的父节点以外,其余节点的event listener都是要收集的。这里面cb(id, traverseUp, arg);
就是指accumulateDirectionalDispatches(domID, upwards, event)
。在遍历过程当中,就是这个方法负责根据domID和所处的阶段(向上遍历就是处在冒泡阶段;向下遍历就是处在捕获阶段)来查找到对应的event listener,而后就该event listener入队到event._dispatchListeners中去。咱们来看看这里的源码:
/** * Tags a `SyntheticEvent` with dispatched listeners. Creating this function * here, allows us to not have to bind or create functions for each event. * Mutating the event members allows us to not have to create a wrapping * "dispatch" object that pairs the event with the listener. */ function accumulateDirectionalDispatches(domID, upwards, event) { if ("production" !== process.env.NODE_ENV) { if (!domID) { throw new Error('Dispatching id must not be null'); } injection.validate(); } var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; var listener = listenerAtPhase(domID, event, phase); if (listener) { event._dispatchListeners = accumulate(event._dispatchListeners, listener); event._dispatchIDs = accumulate(event._dispatchIDs, domID); } } 复制代码
var listener = listenerAtPhase(domID, event, phase);
复制代码
if (listener) { event._dispatchListeners = accumulate(event._dispatchListeners, listener); event._dispatchIDs = accumulate(event._dispatchIDs, domID); } 复制代码
而listenerAtPhase(domID, event, phase)最终调用getListener方法,根据domID(实质就是指reactId)和阶段性事件注册名(好比冒泡阶段:onClick;捕获阶段:onClickCapture)去咱们在第一个阶段所提到的listenerBank这个事件注册登记表里面查找event listener。若是又找到event listener,就将其入队。入队操做是由accumulate()方法完成,本质上就是一个数组的concat。
说到这里,咱们基本上把这个“实例化合成事件对象”这个步骤所涉及的流程梳理清楚了。在这个过程所对应的函数调用栈中,最重要的就是traverseTwoPhase这个函数的调用了。就是在这个函数以上的调用栈中,react保证了event listener入队的两个顺序。哪两个顺序呢?第一个是注册在捕获阶段的event listener要先于冒泡阶段的event listener入队;第二个是注册在各个事件传播阶段的event listener的入队顺序要正确。
关于第一个顺序的保证,上面已经说起过。那就是经过如下两个语句的前后顺序来保证:
traverseParentPath('', targetID, cb, arg, true, false); traverseParentPath(targetID, '', cb, arg, false, true); 复制代码
第二个顺序的保证就是for循环中,经过沿着给定event target的层级关系链向上或向下,逐一遍历,逐一入队来保证的。具体就是经过如下代码来实现:
var traverse = traverseUp ? getParentID : getNextDescendantID; for (var id = start;; /* until break */id = traverse(id, stop)) { if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { cb(id, traverseUp, arg); } if (id === stop) { // Only break //after// visiting `stop`. break; } ) 复制代码
到目前为止,咱们须要调用的event listener已经妥妥地保存在event._dispatchListeners数组里了。一切等待react的调用。那么,下面咱们就来说述第二步骤:“调用event listenter”。
调用流程的入口是下面的这个代码:
ReactUpdates.batchedUpdates(runEventQueueInBatch, events);
复制代码
对应的函数调用栈是:
调用流程发生一个transaction(事务)里面。transaction模式相似于一个wrapper,主要做用其实就是调用一个核心方法。由于本文是深刻react合成事件系统,因此,我不打算阐述transaction的模式与原理,咱们只须要知道,跟event listener调用过程相关的这个核心方法是“runEventQueueInBatch ”方法便可。不讲transaction模式的话,那么event listener调用过程就比较简单了,能够总结为:两个变量,两次循环。
哪两个变量呢?答曰:
eventQueue和event._dispatchListeners都是队列(在javascript中,用数组来实现)。eventQueue在上面提过,当它是数组的时候,那么该数组就是由event object(SyntheticEvent实例)组成的。而event object的_dispatchListeners这个数组又是由咱们的event listener组成。在调用栈中,咱们能够找到这两个负责作循环的方法:
eventQueue |
|--- event1
|--- event2
|--- ......
|--- eventn._dispatchListeners|
|--- listener1
|--- listener1
|--- .........
|--- listenern
复制代码
两个的关系就是如上图示。因此,不难理解,要想调用event listener,则须要通过两次循环(相似于二位数组的双重循环)。从方法名不难看出,调用栈中负责这两次循环的方法是:
通常状况下,eventQueue只有event object,因此,forEachAccumulated(arr, cb, scope)没什么好讲的。由于forEachEventDispatch(event, cb)这个循环中有一个很重要的实现,那就是“阻止事件传播”的事件机制实现。下面,咱们重点看看这个方法的实现代码:
/** * Invokes `cb(event, listener, id)`. Avoids using call if no scope is * provided. The `(listener,id)` pair effectively forms the "dispatch" but are * kept separate to conserve memory. */ function forEachEventDispatch(event, cb) { var dispatchListeners = event._dispatchListeners; var dispatchIDs = event._dispatchIDs; if ("production" !== process.env.NODE_ENV) { validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { if (event.isPropagationStopped()) { break; } cb(event, dispatchListeners[i], dispatchIDs[i]); } } else if (dispatchListeners) { cb(event, dispatchListeners, dispatchIDs); } } 复制代码
一个大大的for循环映入眼帘,相信你也看到了。在for循环里面,cb(event, dispatchListeners[i], dispatchIDs[i]);
实质上就是负责真正地调用(使用调用操做符)event listener的executeDispatch(event, dispatchListeners[i], dispatchIDs[i])
,而一个平淡无奇的“break”关键字倒是实现“阻止事件传播”的事件机制的灵魂之所在。当event object 的isPropagationStopped方法返回值为true的时候,“break”一下,咱们跳出了整个大循环,从而也就不执行队列后面的全部event listener了,从而实现了“阻止事件传播”的事件机制。那何时isPropagationStopped方法的返回值是true呢?咱们不妨全局搜索一下,看看它的实现代码(在SyntheticEvent.js):
stopPropagation: function() { var event = this.nativeEvent; event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; this.isPropagationStopped = emptyFunction.thatReturnsTrue; }, 复制代码
从代码中,咱们能够看出,当队列中前一个event listener中,用户手动调用了这个stopPropagation方法的时候,react就会在event object身上追加一个字段,它的值是一个函数引用,一个返回true值的函数引用。所以,当for循环执行到下一个循环的时候,isPropagationStopped就指向emptyFunction.thatReturnsTrue,if条件就为真,因而跳出整个大循环。
好,在react合成事件系统中,“阻止事件传播”的事件机制是如何实现的,已经讲完了。下面咱们继续往下看。
上面咱们也提到,真正负责调用(使用调用操做符)event listener的方法是executeDispatch(event, dispatchListeners[i], dispatchIDs[i])
。这个executeDispatch方法实际上是一个函数引用。它具体所指能够由如下代码能够看出
/** * Dispatches an event and releases it back into the pool, unless persistent. * * @param {?object} event Synthetic event to be dispatched. * @private */ var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) { if (event) { var executeDispatch = EventPluginUtils.executeDispatch; // Plugins can provide custom behavior when dispatching events. var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event); if (PluginModule && PluginModule.executeDispatch) { executeDispatch = PluginModule.executeDispatch; } EventPluginUtils.executeDispatchesInOrder(event, executeDispatch); if (!event.isPersistent()) { event.constructor.release(event); } } }; 复制代码
结合上面的注释“Plugins can provide custom behavior when dispatching events.”和在EventPluginHub.js里面对EventPluginHub的注释:
/** * This is a unified interface for event plugins to be installed and configured. * * Event plugins can implement the following properties: * * `extractEvents` {function(string, DOMEventTarget, string, object): *} * Required. When a top-level event is fired, this method is expected to * extract synthetic events that will in turn be queued and dispatched. * * `eventTypes` {object} * Optional, plugins that fire events must publish a mapping of registration * names that are used to register listeners. Values of this mapping must * be objects that contain `registrationName` or `phasedRegistrationNames`. * * `executeDispatch` {function(object, function, string)} * Optional, allows plugins to override how an event gets dispatched. By * default, the listener is simply invoked. * * Each plugin that is injected into `EventsPluginHub` is immediately operable. * * @public */ var EventPluginHub = { // ...... } 复制代码
咱们能够看出,最后的executeDispatch引用的计算规则是这样的:若是某某 eventPlugin实现了这个方法,则首先使用它。不然,就使用默认的方法。默认的executeDispatch是怎样的呢?在原文件EventPluginUtils.js里面,咱们找到了它:
/** * Default implementation of PluginModule.executeDispatch(). * @param {SyntheticEvent} SyntheticEvent to handle * @param {function} Application-level callback * @param {string} domID DOM id to pass to the callback. */ function executeDispatch(event, listener, domID) { listener(event, domID); } 复制代码
可见,默认的executeDispatch的实现是最简单的,也就是说使用函数调用操做符去操做咱们的event listener。
纵观全部的eventPlugin,好像只有SimpleEventPlugin实现了本身的executeDispatch方法:
/** * Same as the default implementation, except cancels the event when return * value is false. * * @param {object} Event to be dispatched. * @param {function} Application-level callback. * @param {string} domID DOM ID to pass to the callback. */ executeDispatch: function(event, listener, domID) { var returnValue = listener(event, domID); if (returnValue === false) { event.stopPropagation(); event.preventDefault(); } }, 复制代码
由于SimpleEventPlugin处理了大部分的事件类型,因此,通常状况下,上面提到的那个引用指向的就是SimpleEventPlugin的executeDispatch方法。
咱们目光放在if条件语句中:
if (returnValue === false) { event.stopPropagation(); event.preventDefault(); } 复制代码
联系这段代码的上下文,咱们能够得知,咱们平日react开发过程当中,经过在event listener返回false来阻止事件传播和取消默认行为就是经过这段代码来实现的。从这段代码,咱们也知道,在event listener中返回false,就是至关于react帮咱们在event object身上调用了stopPropagation方法。因此,咱们能够有如下结论:在react应用中,若是你想阻止事件传播,你有两种方式:
现在,咱们已经明明白白地看到了对event listener的调用了:
listener(event, domID)
复制代码
从以上代码,咱们能够看出,在reactV0.8.0中,咱们的event listener实际上是被传入两个实参的,只不过当时第二个参数reactId不多人用罢了。
说到这里,咱们已经梳理到调用event listener流程的末端了,也就是说,第三阶段的总体分析也完成了。整个第三阶段有如下的几个研究重点,下面回顾一下:
收尾阶段主要是对eventQueue和event object(当前event loop dispatch的那个)所占据的内存进行释放。在javascript中,释放内存无非就是把某个变量赋值为null。
首先,咱们看看eventQueue的内存释放(在EventPluginHub.js中):
processEventQueue: function() { // Set `eventQueue` to null before processing it so that we can tell if more // events get enqueued while processing. var processingEventQueue = eventQueue; eventQueue = null; forEachAccumulated(processingEventQueue, executeDispatchesAndRelease); ("production" !== process.env.NODE_ENV ? invariant( !eventQueue, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.' ) : invariant(!eventQueue)); } 复制代码
而后,咱们来看看event object的内存释放。
第一步,执行完全部的event listener后,清空一下_dispatchListeners和_dispatchIDs这两个队列:
/** * Standard/simple iteration through an event s collected dispatches。 * */ function executeDispatchesInOrder(event, executeDispatch) { forEachEventDispatch(event, executeDispatch); event._dispatchListeners = null; event._dispatchIDs = null; } 复制代码
第二步,结合pooling技术作内存释放:
var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) { if (event) { var executeDispatch = EventPluginUtils.executeDispatch; // Plugins can provide custom behavior when dispatching events. var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event); if (PluginModule && PluginModule.executeDispatch) { executeDispatch = PluginModule.executeDispatch; } EventPluginUtils.executeDispatchesInOrder(event, executeDispatch); if (!event.isPersistent()) { event.constructor.release(event); } } }; 复制代码
咱们能够看到,若是用户没有手动去持久化(event.isPersistent=function(){ return true})这个event object的话,那么这个event object就会被释放掉(release)。怎么释放呢?咱们拿当前的event object是SyntheticMouseEvent的实例的这种状况举个例子,那么event.constructor就是指SyntheticMouseEvent类:
function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent) { SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent); } 复制代码
从上面能够看出,SyntheticMouseEvent实质上是继承SyntheticUIEvent的,而SyntheticUIEvent又是继承SyntheticEvent。咱们觉得会在SyntheticEvent的代码里面找到了release方法的实现代码,实际上是在PooledClass.js里面找到的。为何呢?由于release方法是react把SyntheticEvent加入到pooling池后动态添加的静态方法:
PooledClass.addPoolingTo(SyntheticEvent, PooledClass.threeArgumentPooler);
复制代码
而addPoolingTo方法的实现代码是这样的:
/** * Augments `CopyConstructor` to be a poolable class, augmenting only the class * itself (statically) not adding any prototypical fields. Any CopyConstructor * you give this may have a `poolSize` property, and will look for a * prototypical `destructor` on instances (optional). * * @param {Function} CopyConstructor Constructor that can be used to reset. * @param {Function} pooler Customizable pooler. */ var addPoolingTo = function(CopyConstructor, pooler) { var NewKlass = CopyConstructor; NewKlass.instancePool = []; NewKlass.getPooled = pooler || DEFAULT_POOLER; if (!NewKlass.poolSize) { NewKlass.poolSize = DEFAULT_POOL_SIZE; } NewKlass.release = standardReleaser; return NewKlass; }; 复制代码
看到了没? NewKlass.release = standardReleaser;
语句中的NewKlass就是指SyntheticEvent。因此,到最后,event.constructor.release
的release指向的是standardReleaser。因而,咱们来看看standardReleaser是什么样的呢:
var standardReleaser = function(instance) { var Klass = this; if (instance.destructor) { instance.destructor(); } if (Klass.instancePool.length < Klass.poolSize) { Klass.instancePool.push(instance); } }; 复制代码
参数instance在实参阶段就是SyntheticMouseEvent实例。因而,咱们沿着SyntheticMouseEvent实例的原型链上查找一下这个destructor方法,终于找他它了(在SyntheticEvent.js中):
/** * `PooledClass` looks for `destructor` on each instance it releases. */ destructor: function() { var Interface = this.constructor.Interface; for (var propName in Interface) { this[propName] = null; } this.dispatchConfig = null; this.dispatchMarker = null; this.nativeEvent = null; } 复制代码
咱们能够看到,对event object的内存释放工做,主要是把它的各个字段所引用的内存所释放,而并无对它自己所占据的内存进行释放。event object最终是被pooling技术所管理的,也就是说,它最终会被回收到实例池中,见standardReleaser方法中的下面代码片断:
if (Klass.instancePool.length < Klass.poolSize) { Klass.instancePool.push(instance); } 复制代码
说到这里,收尾阶段已经分析完了。下面再说多一点。那就是在下一次event loop开始的时候,react是如何从实例池中取回实例的呢?其实这就衔接回咱们的第三阶段的第一步骤:合成event object。由于每个event loop里面,都要从新执行extractEvent方法去合成一个event object,而extractEvent方法都会从实例池中取回实例的一行代码,好比:
var event = SyntheticEvent.getPooled(
eventTypes.change,
targetID,
nativeEvent
);
复制代码
而这个getPooled方法实际上是在代码初始化阶段,把这个类(好比:SyntheticEvent)加入到pooling池中就决定的。也就是咱们上面提到的addPoolingTo方法调用时中传入的PooledClass.threeArgumentPooler方法。那咱们就来看看PooledClass.threeArgumentPooler这个方法的实现代码:
var threeArgumentPooler = function(a1, a2, a3) { var Klass = this; if (Klass.instancePool.length) { var instance = Klass.instancePool.pop(); Klass.call(instance, a1, a2, a3); return instance; } else { return new Klass(a1, a2, a3); } }; 复制代码
看到这里,咱们心中想要的答案就明朗了。所谓的“getPooled”就是从被pooling化的类的实例池(实例池是一个数组)中pop一个实例对象出来,并从新对它进行初始化而已。也就是下面的两行代码:
var instance = Klass.instancePool.pop();
Klass.call(instance, a1, a2, a3);
复制代码
讲到这里,不知道你明白了没?对于event object,咱们在event loop的收尾阶段把它放回实例池:Klass.instancePool.push(instance);
。在下一个event loop的开始时候又从新把它拿出来:var instance = Klass.instancePool.pop();
。
四个阶段的梳理与讲解已经完毕了。下面咱们来作个简单的总结。
经过不断地明确研究点,而后反复写代码,反复地去调试和验证,我收获了不少深入的认知。正是这些深入的认知,使得我揭开了react合成事件系统的神秘面纱,清晰地看见了它的真实面目。与此同时,我也加深了对原生事件机制的理解。
下面说说个人收获:
绞尽脑汁,我就总结这么多了。虽然,本文探索的是reactV0.8.0的合成事件系统,可是我相信即便版本已经更迭到v16.12.0,合成事件系统的主要架构和运行时原理都是没有多大的变化的。
整片文章下来,我相信大致的流程梳理得也算明朗,可是有一些细节是没有深刻的。好比说,各个合成事件对象构造函数的实现细节,pooling技术细节,transaction(事务)的技术细节等等。正所谓,书不尽言,但愿你们也都去探索探索。若是在阅读过程发现观点错误,还请不吝指教和勘正。
谢谢阅读,好走不送。