本人研究的源代码是0.8.0版本的,可能跟最新版本的事件系统有点出入。
什么是合成事件系统?
首先,合成事件这个名词是从“Synthetic Event”翻译过来的,在react的官方文档和源码中,这个术语狭义上是指合成事件对象,一个普通的javascript对象。而在这里,我们谈论的是由众多不同类型事件的合成事件对象组成的合成事件系统(React’s Event System)。在我的理解里面,合成事件是相对浏览器原生的事件系统而言的。合成事件系统本质上是遵循W3C的相关规范,把浏览器实现过的事件系统再实现一遍,并抹平各个浏览器的实现差异,使得开发者使用起来体验是一致的。
在开始理解什么是合成事件系统之前,我们不妨看看我翻译的react的合成事件对象。从这篇文档,我们可以得到以下关于合成事件系统与原生事件系统异同方面的结论:
相同点
- 在
event target
,current event target
,event object
,event phase
和propagation path
等核心概念上的定义是一致的。 - dispatch机制是一致的:一个事件的触发,都会导致某个event object沿着
propagation path
上传播。换句话说,就是同一个propagation path
上的每一个event listener拿到的event object
都是同一个。
也就是说,这两者采用的架构,实现的接口都是一致的。因为两者都遵循W3C的标准规范。
不同点
- 注册方式不一致。
- 拿同样是通过行内attribute来注册事件监听器的DOM Level 1来说,原生事件系统中,属性名都是小写,比如:“onclick”,“onmousedown”等,但是在react的合成事件系统中,是采用小驼峰的写法,比如:“onClick”,"onMouseDown"(注意,"onMouseDown"不要写成“onMousedown”了)。
- 如果把jsx也勉强看作markup language的话(因为jsx最终是会被转换为普通的js代码,所以说如果),原生事件系统目前有从DOM Level 1(其实还有个DOM Level 0,不过它不算实际上的标准,它指的是IE4和Netscape Navigator 4.0最初支持的DHTML)到DOM Level 3的三种事件注册的方式,但是在react的合成事件系统中,只有上述的行内属性的注册方式。
- 对于捕获事件的注册方式不同。在原生事件系统中,我们是通过DOM Level 3的addEventListener()方法的第二个boolean类型的参数来指示是否要把event listener绑定在捕获阶段来实现的。但是在react合成事件系统中,你要想绑定在捕获阶段,则是使用形如“onClickCapture”的属性名。
- 事件监听器(event listener)中,this的指向不同。在原生的事件系统中,事件监听器中this是指向current event target的。而在react的合成事件系统中,this指向的是当前的组件实例。
- 事件监听器(event listener)中,event object不同。在react的合成事件系统中,我们拿到的event object是原生的event object的wrapper。更加具体点说,原生的event object是作为一个key(key名为nativeEvent)挂载在合成事件对象上的。或者换句话说,合成事件对象是原生事件对象的父集。
- 相比于原生事件对象,react的合成事件系统对合成事件对象引入了pooling技术。这么干的原因,用官方的原话说,就是:“These systems should generally use pooling to reduce the frequency of garbage collection.”。
在这里之所以要提到react合成事件系统与原生系统上的异同点,这是因为我觉得带着“造成两者之间的差别的原因是什么呢?”这个疑问去探索react的合成事件系统会更有针对性。因为源码往往是繁复的,如同茫然而无边际的原始森林一般,一旦我们没有目标,就容易迷失在这原始森林里,最终一无所获。
合成事件系统的架构
在ReactEventEmitter.js的源码中,官方给出了这样的架构图:
+------------+ .
| 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
从官方给出的架构图,我们可以看到以下主要的角色:
- DOM(此处,可以等同于浏览器这个大环境)
- application (应用程序,可以理解为开发者写的代码)
- ReactEventEmitter(事件发射器,主要用于桥接DOM和application)
- EventPluginHub (用于收纳各种eventPlugin的hub)
- xxxEventPlugin (负责各种类型的事件对象的合成)
- CallbackRegistry (callback的注册表,主要负责存储,查找我们写的event listenrer)
下面我们来分析一下他们之间的关系。
注意,原架构图,是没有从ReactEventEmitter到DOM的关系链的,是本人添加的。
主要关系
-
从ReactEventEmitter指向DOM的关系是指,ReactEventEmitter负责向DOM的顶层进行事件委托。该关系链对应的大体调用栈是(序号越小,表示越先调用):
- ReactMount.prepareEnvironmentForDOM()
- ReactEventEmitter.ensureListening()
- ReactEventEmitter.listenAtTopLevel()
- EventListenter.listen()
- 在document对象(所谓的topLevel)调用原生的addEventListener或者attachEvent方法进行事件监听。
-
“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)并调用它。 该关系链对应的大体调用栈是:
- 原生事件的触发导致document对象上的TopLevelCallback的调用。
- ReactEventEmitter.handleTopLevel()
- EventPluginHub.extractEvents()
- CallbackRegistry.getListener()
-
“application -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry”这条关系存在于application event listener存储阶段。ReactEventEmitter负责在react component的首次挂载阶段对开发者写的event listener进行收集和存储。在我看来,只需要存在“application -> CallbackRegistry”的关系就好,不知道源码中为什么利用引用传递,绕来绕去,把整个关系链延伸得这么长。该关系链对应的大体调用栈是:
- ReactDOMComponent._createOpenTagMarkup()。
- ReactEventEmitter.putListener()
- ReactEventEmitter.putListener引用EventPluginHub.putListener
- EventPluginHub.putListener引用CallbackRegistry.putListener
- CallbackRegistry.putListener()
-
“xxxEventPlugin”与EventPluginHub的关系。
各种“xxxEventPlugin”是被注入(inject)到EventPluginHub里面的,换句话说,就是“xxxEventPlugin”的引用会被挂载在EventPluginHub.registrationNames对象的各个key上。注入后的具体数据结构是这样的:
EventPluginHub.registrationNames = { onBlur: xxxEventPlugin, onBlurCapture: xxxEventPlugin, onChange: xxxEventPlugin, onChangeCapture: xxxEventPlugin, ...... }
在v0.8.0的源码中,eventPlugin主要有以下几个:
- SimpleEventPlugin
- EnterLeaveEventPlugin
- ChangeEventPlugin
- CompositionEventPlugin
- MobileSafariClickEventPlugin
- SelectEventPlugin
既然“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用到的构造函数有以下:
- SyntheticEvent
- SyntheticKeyboardEvent
- SyntheticFocusEvent
- SyntheticMouseEvent
- SyntheticTouchEvent
- SyntheticUIEvent
- SyntheticWheelEvent
- SyntheticClipboardEvent
EventPluginHub,eventPlugin和SyntheticEvent三者之间的关系如下:
在对每个阶段进行分析前,我先做个预告。预告一下几种数据结构和两种关系。
数据结构
- eventQueue(队列,用javascript的数组来实现)。
- SyntheticEvent实例(对象)
- listenerBank(对象),形如:
listenerBank = {
onClick: {
'[0].[1]': listener // listener就是我们挂载在jsx的事件回调
},
onClickCapture: {
'[0].[1]': listener
}
}
两种关系
- EventPluginHub与各种eventPlugin是接口与实现的关系;
- SyntheticEvent与各种SyntheticXXXEvent是继承关系。
四个阶段
正如上面我提到的,阅读源码必须有一个聚焦的目标。我们不妨从注册在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池中。
准备阶段
准备阶段做了两件事:
- 依赖注入
- 事件委托(event delegation)
依赖注入
因为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。
EventPluginOrder
我们直译就是“事件插件顺序”的意思。其实,更确切地说,应该是指“事件插件的加载顺序”。这个被注入的顺序是怎么的呢?见源码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
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”。事件委托的整体流程大体如下:
- 在需要事件监听的元素的祖先层级中,对所有类型的事件进行事件监听。
- 用户在非原生元素(比如jq对象,react element)上注册event listener。
- 类库负责收集并存储这种对应关系,登记在事件监听登记表。
- 原生事件触发时,执行事先注册好的事件回调。而执行事件回调的过程就是根据event target去事件监听登记表查找对应的event listener,并按照正确的顺序去调用它们的过程。
此时,我们基本上可以看清“委托(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是注册在事件的冒泡阶段,所以我们可以得出以下结论:
- 绑定在react element上的event listener,无论是注册在冒泡阶段还是捕获阶段,它的执行都是要比documen对象上注册在冒泡阶段的topLevelCallback要晚;
- 绑定在react element不同事件的event listener的执行顺序跟绑定在documen对象上不同事件的topLevelCallback执行顺序保持一致。
下面,我以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”为前缀的事件名的集合。打印出来,是这样的:
对的,putListener就是react收集我们event listener的入口。在继续往下追查之前,我们不妨自问一下:“react什么时候开始收集我们的event listener呢?”。这个问题就是转换为:“_createOpenTagMarkup方法什么时候会被调用呢?”。答曰:“每个reactDOMComponent在首次挂载的时候,都会调用_createOpenTagMarkup方法”。也就是说,每个组件在初次挂载之前,都会先收集用户注册的event listener。好,我们明白了收集的时机,接下来,我们就是要弄清楚,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 element上注册事件监听,即建立DOM元素 -> 事件名 -> event listener这三者的关系。
- react在组件初始挂载的时候,就会把这种关系收集并组织起来,记录在listenerBank模块全局变量中。如何组织呢?那就是,在listenerBank对象的第一层按照注册事件类型进行分类,在同一个事件类型里面又按照reactid进行分类。
到这里,react合成事件系统的第二阶段算是讲完了,我们只需要记住listenerBank对象的数据结构就好,以便于在第三阶段讲解涉及取回event listener时能有很好的理解。
其实,相比event listener的调用阶段(也就是第三阶段),上面提到的一,二阶段都可以算作准备工作。因为,到目前为止,我们的event listener还乖乖地躺在listenerBank的怀抱里面沉睡呢。
调用阶段
重头戏终于来了。从event listener的函数签名void func(event)
可以得知我们第四阶段有以下的两个探索点:
- 合成event object;
- 调用event listenter。
事实上,调用阶段的入口函数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”又分两步走:
- 实例化合成事件对象。
- 将要调用的event listener保存在这个对象上。
所有的eventPlugin核心实现的就是上面两个功能需求。我们不妨抽取两三个plugin来看看。
- SimpleEventPlugin
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再回过头来好好分析。下面,我们继续抽样。
- SelectEventPlugin(源码摘取自SelectEventPlugin.js)
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()
;
- ChangeEventPlugin(源码摘取自SelectEventPlugin.js)
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所要实现的核心两个功能需求就是:
- 实例化合成事件对象。
- 将要调用的event listener保存在这个对象上。
因为,实例化合成事件对象这个过程包含了很多实现细节,比如应对浏览器差异而做的兼容细节,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);
}
}
- 执行event listener的查找是以下的语句:
var listener = listenerAtPhase(domID, event, phase);
- 将event listener入队到event._dispatchListeners是以下语句:
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
eventQueue和event._dispatchListeners都是队列(在javascript中,用数组来实现)。eventQueue在上面提过,当它是数组的时候,那么该数组就是由event object(SyntheticEvent实例)组成的。而event object的_dispatchListeners这个数组又是由我们的event listener组成。在调用栈中,我们可以找到这两个负责做循环的方法:
- forEachAccumulated()
- forEachEventDispatch()
eventQueue |
|--- event1
|--- event2
|--- ......
|--- eventn._dispatchListeners|
|--- listener1
|--- listener1
|--- .........
|--- listenern
两个的关系就是如上图示。所以,不难理解,要想调用event listener,则需要经过两次循环(类似于二位数组的双重循环)。从方法名不难看出,调用栈中负责这两次循环的方法是:
- forEachAccumulated(arr, cb, scope)
- forEachEventDispatch(event, cb)
一般情况下,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中,手动调用event.stopPropagation();
- 在event listener中,通过return false来让react帮我们调用event.stopPropagation()(在v0.12.0这个版本中,这种方式被废弃了。你可以在这篇blog里面全局搜索这句话:"DEPRECATED Returning false from event handlers to preventDefault");
如今,我们已经明明白白地看到了对event listener的调用了:
listener(event, domID)
从以上代码,我们可以看出,在reactV0.8.0中,我们的event listener其实是被传入两个实参的,只不过当时第二个参数reactId很少人用罢了。
说到这里,我们已经梳理到调用event listener流程的末端了,也就是说,第三阶段的整体分析也完成了。整个第三阶段有以下的几个研究重点,下面回顾一下:
- event listener是如何收集的?
- event listener的调用顺序是如何得以保证的?
- 阻止事件传播机制是如何实现的?
- 在event listener中return false,react到底帮我们做了什么?
收尾阶段
收尾阶段主要是对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合成事件系统的神秘面纱,清晰地看见了它的真实面目。与此同时,我也加深了对原生事件机制的理解。
下面说说我的收获:
- top level其实就是document对象;
- 高大上的react合成事件系统中,listen at top level其实是很简单,粗暴地,罗列式地堆积监听代码的一个过程。
- 相信很多人不知道,无论是原生还是react的合成事件系统中,“dispatch”这个概念是意味着同一个事件传播路径上所有的event listener都公用同一个event object。
- event object是现场(这个现场就是相对于原生事件的触发时机)合成的。
- 对一些生僻的用法(比如:event.nativeEvent, event.persistent(),注册捕获事件用onXXXCapture这种写法)有了更深一步的认识。
- 原生事件系统跟react的合成事件系统混用容易踩到坑里面。因为,注册到原生事件系统中的event listener与注册到react合成事件系统中的event listener是分别在两个不同的体系中的。对于这一点,我们必须有清晰的认知。我们得知道,这两个系统的桥接点是在document对象上。知道了这些,我们就能知道一下结论:
- 在react合成事件系统中,我们不能阻止事件传播到原生事件系统中(是的,event.nativeEvent.stopPropagation()也不行)。
- 但是,在原生事件系统中,我们却能阻止事件传播到react合成事件系统中去。
绞尽脑汁,我就总结这么多了。虽然,本文探索的是reactV0.8.0的合成事件系统,但是我相信即使版本已经更迭到v16.12.0,合成事件系统的主要架构和运行时原理都是没有多大的变化的。
整片文章下来,我相信大体的流程梳理得也算明朗,但是有一些细节是没有深入的。比如说,各个合成事件对象构造函数的实现细节,pooling技术细节,transaction(事务)的技术细节等等。正所谓,书不尽言,希望大家也都去探索探索。如果在阅读过程发现观点错误,还请不吝指教和勘正。
谢谢阅读,好走不送。