Que's Blog

WebFrontEnd Development

0%

react Event

写在最前

这里暂时不做高屋建瓴式的解读直接给出关键文件。而是从之前已有的分析开始,尝试自己蛛丝马迹找出来。

既然是事件绑定,那么必然在ReactDom.render的环节里面有处理。所以这里简单回顾然后把它挖出来。

我们知道React在浏览器的运行时里面的渲染存在两种情况,一种是第一次的初次渲染,一种是props更新带来的更新。

当我们说事件的时候,我们是在说谁的事件?之前分析已经提到了调度函数ReactReconciler返回的几种实例:ReactDOMEmptyComponentReactDOMComponent实例、ReactCompositeComponentWrapper实例。

那么回想一下,当我们绑定事件的时候第一次渲染里面必然会有,props和state更新时候我们的React事务也会这样做(清场和现场还原)。

这个操作呢,第一次渲染时候大抵在ReactCompositeComponentWrapper.mountComponent里面,它可能是这个组件上绑定,再不济也应该是在其children中绑定,后面的渲染,则大抵在ReactUpdates.jsReactUpdatesFlushTransaction中。

不过整体按React抽象至死的尿性,显然最后都会殊途同归。不过这里还是尝试整理一下两条路线下的调用路径。

mountComponent路径

这里不再重复贴代码,如果读到这里的时候没有看过之前的分析,也没有自己打开IDE debug过,其实这篇文章读与不读,本质上区别不大。

这个函数代码不多,配合这个函数的注释简直确认了我自己猜测的注释:

1
2
3
4
5
6
7
8
9
10
11
/**
* Initializes the component, renders markup, and registers event listeners.
*
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {?object} hostParent
* @param {?object} hostContainerInfo
* @param {?object} context
* @return {?string} Rendered markup to be inserted into the DOM.
* @final
* @internal
*/

简单分析ReactCompositeComponent.mountComponent可以很容易知道这里的事件绑定,下一步路径是ReactCompositeComponentWrapper.performInitialMount

在这个函数内

1
2
3
4
5
var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
);
this._renderedComponent = child;

var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID);

child是自定义函数里面的render里面的子组件,他的type是一个string,这样调度模块使用ReactDOMComponent.mountComponent来定义这块。

回顾一下ReactDOMComponent._updateDOMProperties 里面提到的enqueuePutListener,这里是事件注册的关注点。这个函数里面进行绑定的关键代码:

1
2
3
4
5
6
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});

这里事件绑定到document上,然后使用putListener来进行事件绑定。

其中listenTo指向ReactBrowserEventEmitter.listenToputListener指向EventPluginHub.putListener

props更新路径

参考之前lifecycle里面props更新环节的render分析和ReactCompositeComponent._updateRenderedComponent分析。这里的定位分析点是起始点ReactCompositeComponent._updateRenderedComponent

这里有替换和更新两个分支 我们这里只谈更新。

1
2
3
4
5
6
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context),
);

和之前同样的道理,定位到ReactDOMComponent.receiveComponent。一路顺着

1
2
3
ReactDOMComponent.receiveComponent
-> ReactDOMComponent.updateComponent
-> ReactDOMComponent._updateDOMProperties

的路径,显然,最初的猜测一点没错。

补充 & 旁白

这里两个路径的探索,说白了还是从断点角度的摸索,属于从下往上的摸索。虽然可以摸清楚这些,但是实际上不是最好的办法。

但是如果可以对虚拟DOM节点、render、和Diff路径有相对清晰的了解和认知,那么当可以从顶层设计上对这个ReactDOMComponent._updateDOMProperties函数进行定位。

如果非要一个思路,这里可以这样思考:

  • Diff算法实质上除了’移动’,不存在实质意义上的’更新’,这个更新本质是删除旧的+替换新的。
  • 不管是初始render还是替换,都需要对一个新的ReactDomCompositeComponent进行实例化备用。
  • 这个过程中终究会伴随_updateDOMProperties函数调用。
  • 这个_updateDOMProperties函数,是自定义组件渲染为ReactDOMComponent的核心调用。

正式开始

事件保存 && 绑定

保存

事件保存主要是走的EventPluginHub.putListener,将putListener保存到了listenerBank。具体一点来说,是将事件放到了listenerBank[registrationName][getDictionaryKey(inst)]。这里getDictionaryKey的返回值是'.' + inst._rootNodeID

绑定

来自ReacDomComponent.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return;
}
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment =
containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
var doc = isDocumentFragment
? containerInfo._node
: containerInfo._ownerDocument;
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});
}

这个绑定是由listenTo(registrationName, doc)处理的,直接把事件绑定到document,利用事件冒泡性质来进行事件委托处理。这个listenTo实际上就是ReactBrowserEventEmitter.listenTo

事件触发 && 分发

事件触发这个实际上在上一小结就提到了,当事件在某个元素上触发,会冒泡到document,然后document开始触发相关逻辑。

事件分发主要是通过ReactBrowserEventEmitter.listenTo处理。这里关于浏览器兼容有点多,不过这里管住核心代码就好。这里的listenTo有两个核心调用, 都在ReactBrowserEventEmitter.ReactEventListener上。

ReactEventListener

  • trapBubbledEvent
  • trapCapturedEvent

看看它们引用到的listen和capture中的listen(React 15.6是支持冒泡+捕获两种事件传播的,具体参考traverseTwoPhase函数,但是这仅仅是传播的支持,并没有具体实现这个捕获事件触发)。这里就是我们熟悉的原生API了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
listen: function (target, eventType, callback) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
return {
remove: function () {
target.removeEventListener(eventType, callback, false);
}
};
} else if (target.attachEvent) {
target.attachEvent('on' + eventType, callback);
return {
remove: function () {
target.detachEvent('on' + eventType, callback);
}
};
}
}

这里注意的是我们这里的addEventListener是在DOM层级上触发的,当我们触发了这个事件,接下来需要找到我们保存的回调函数,不然这个触发就毫无意义。

trapBubbledEvent为例:

1
2
3
4
5
6
7
8
9
10
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element,
handlerBaseName,
ReactEventListener.dispatchEvent.bind(null, topLevelType),
);
},

这里传入到listen中的事件回调是ReactEventListener.dispatchEvent.bind(null, topLevelType)

首先,这里有个十分重要的地方需要注意,那就是这个element变量。这个element常规情况下直接指向的document对象。也就是说,事件绑定在document上了。这可以方便整体的事件管理,原理同大家用过的Jquery事件委托。

其次,这里进一步看看ReactEventListener.dispatchEvent的基础定义:

1
2
3
4
5
6
7
8
9
10
11
dispatchEvent: function (topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
try {
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}

EventListener.listen本质上还是addEventListener的封装,所以作为回调的函数ReactEventListener.dispatchEvent.bind(null, topLevelType)它会被传入一个event事件对象 ,这个对象会被作为nativeEvent参数传入到TopLevelCallbackBookKeeping然后就是重点了: event对象中有触发的DOM node节点引用,这点非常重要,只有如此,我们才可以进一步在虚拟DOM树中获取到需要触发冒泡捕获的所有节点,这点同样非常重要。

PS: 除了这里常规的事件委托,关于SimpleEventPlugin.putListener可以了解一下。

dispatchEvent

ReactEventListener.dispatchEvent这里做的事情显然是触发我们定义好的回调,我们所有的事件绑定的回调函数都被储存在一个集中的地方,那么我们现在看看它如何从这个集合中正确找出我们需要的事件回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatchEvent: function(topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}

var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
topLevelType,
nativeEvent,
);
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
},

关于ReactUpdates.batchedUpdates之前在render相关文章里面有分析,这里不做更多分析,这里的关键是handleTopLevelImpl

handleTopLevelImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function handleTopLevelImpl(bookKeeping) {
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 关于这个函数 用于从原生元素上获取最近的上级ReactNode实例
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
nativeEventTarget,
);

// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
// 提前缓存祖先组件,因为事件回调可能会中途修改它,导致无法找对正确对应ReactNode实例
var ancestor = targetInst;
do {
bookKeeping.ancestors.push(ancestor);
ancestor = ancestor && findParent(ancestor);
} while (ancestor);

for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
ReactEventListener._handleTopLevel(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent),
);
}
}

这里do~while先缓存target触发事件那一瞬的所有父组件,这是一个数组,从左到右,右边的是左边的父组件。这里i从0起,其实也就是事实上走了从子到父的路径,也就是说,只有冒泡被实现了。这里_handleTopLevel指向ReactEventEmitterMixin.handleTopLevel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handleTopLevel: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
var events = EventPluginHub.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
runEventQueueInBatch(events);
}

这里有extractEvents从注册的事件里面将目标事件取出,然后调用runEventQueueInBatch批量执行回调。其内容仍旧是EventPluginHub上的:

1
2
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);

接下来看看这两个函数细节。enqueueEvents作用相对简单,维护EventPluginHub.eventQueue变量确保其是一个事件组成的一维数组(也可能是null)。

processEventQueue函数则是触发所有事件,将事件清空,并抛出其运行时的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

processEventQueue: function(simulated) {
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseSimulated,
);
} else {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
ReactErrorUtils.rethrowCaughtError();
},

这里executeDispatchesAndReleaseSimulatedexecuteDispatchesAndReleaseTopLevel都是调用的executeDispatchesAndRelease只不过一个传入了true参数,一个传入了false参数。

一路深入这个函数的调用栈

1
2
3
4
executeDispatchesAndRelease
->EventPluginUtils.executeDispatchesInOrder
-->executeDispatch
--->invokeGuardedCallback

其实他们最终调用到的都是同一个函数invokeGuardedCallback。这个函数作用就是直接执行eventHandle,并捕获错误,没别的作用了。

小总结

前面分析的都是具体的路径,不过总归对触发还是需要一个原理上的总结。

我们知道事件在DOM中的传播是先从外层逐层捕获,然后从里层逐层冒泡。这个过程涉及到的DOM层实际上就是触发事件的target node,和其上级的各个父祖元素。这是其一。

其二,当冒泡捕获发生以后,会在各个父祖元素上也触发对应的事件回调。

其三,当我们在document上触发事件时候,事件回调会被传入一个event对象,这个event对象里面会有触发事件的target node(HTMLElement)的引用,配合ReactDOMComponentTree的API或者其他,总之精确获取到要触发冒泡的目标组件集合就可以做到顺手拈来。

最后,触发的那瞬间在储存的事件集合里面找到对应inst._rootNodeID的事件并触发即可。当然,这里的触发也会进行批处理优化(runEventQueueInBatch)。

这就是整体的原理。至于细节,可以慢慢从上面的分析上思考。

源码细节

EventPluginHub

API 说明
putListener 保存listener到集合
getListener 从集合中获取listener
deleteListener 从集合删除这个listener
deleteAllListeners 移除集合里面所有listener
extractEvents 允许已注册插件从浏览器原生事件获取已注册事件
enqueueEvents 将事件排队推入一个合成事件中,在processEventQueue执行时触发
processEventQueue 执行事件队列上的所有合成事件

extractEvents

extractEvents方法可能是整个流程中比较影响深入理解的,这里简单分析一下。

这个函数实质上遍历EventPluginRegistry.plugins,然后通过根据其内部的plugin自身的extractEvents方法来获取事件。以SimpleEventPlugin为例。这里假设TopLevelTypes === ‘topError’ 。此时这个代码的逻辑就相当于:

1
2
3
4
5
6
7
8
var event = SyntheticEvent(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget
)
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;

此时event的数据结构是这样的

EventPropagators.accumulateTwoPhaseDispatches(event)顺藤摸瓜则指向了:

1
2
3
4
5
EventPluginUtils.traverseTwoPhase(
event._targetInst,
accumulateDirectionalDispatches,
event,
);

其最终的指向则是ReactDOMTreeTraversal.traverseTwoPhase,这个函数模拟冒泡/捕获环节的事件分发的遍历行为,实质上它是获取所有父元素得到数组(索引升序对应更上层的父元素),先降序遍历触发捕获回调,然后升序遍历触发冒泡回调的过程。而这个回调则指向了accumulateDirectionalDispatches

1
2
3
4
5
6
7
8
9
10
function accumulateDirectionalDispatches(inst, phase, event) {
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}

这个函数整体来说做了两件事就是操作event._dispatchListeners && event._dispatchInstances

在之前我们提到traverseTwoPhase是冒泡捕获的遍历过程的模拟,对应的说这里的phase参数就是这个过程的标志,capturedbubbled是其可选值。之所以有这个参数 就是因为在不同阶段对应的listener并不一样。**listenerAtPhase实质上调用EventPluginHub.getListener来获取这个listener**。

其他

除了这些,这里还有一个injection方法,通过EventPluginRegistry上的方法来进行一些依赖注入。

这个注入还是在之前见过很多次的ReactDefaultInjection.js里面。这里整体代码是这样的,为了方便阅读有简单修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ReactInjection.EventPluginHub.injectEventPluginOrder([
'ResponderEventPlugin',
'SimpleEventPlugin',
'TapEventPlugin',
'EnterLeaveEventPlugin',
'ChangeEventPlugin',
'SelectEventPlugin',
'BeforeInputEventPlugin',
]);

ReactInjection.EventPluginHub.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});

EventPluginRegistry

EventPluginHub是一个Hub,更多时候它只是一个调度。而EventPluginRegistry,是EventPlugin实际上的数据源,上面提到的注入实际上还是注入到这里,所以还是看看里面东西。

API 说明
injectEventPluginOrder 注入 插件排序的次序表 这里仅仅是插件名称(Array)
injectEventPluginsByName 注入 插件本体 这个必须在次序表中
getPluginModuleForEvent 查找提供的事件的plugin
_resetEventPlugins 单元测试专用API

总结&思考

关于React的事件系统实际上源码里面(ReactBrowserEventEmitter)有注释,并且还有一个流程图示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactEventListener, which is injected and can therefore support pluggable
* event sources. This is the only work that occurs in the main thread.
*
* - 顶级委派用于捕获大多数本机浏览器事件。
* 这可能只发生在主线程中 并且这是ReactEventListener的主要职责。
* ReactEventListener是注入的,因此可以支持可插入的事件源。 这是浏览器主线程中发生的唯一工作。
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* 我们对事件进行规范化和去重处理 以解决浏览器怪癖问题。这可以在工作线程中完成。
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - 将这些浏览器原生事件(以及用于捕获它的相关顶级类型)转发给`EventPluginHub`
* 它会询问`EventPluginHub`是否保存有它所需要提取的合成事件
*
*
* - The `EventPluginHub` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - 然后,`EventPluginHub`将通过使用“dispatches”来处理每个事件
* 一系列侦听器和IDs 关联到这些事件。
*
* - The `EventPluginHub` then dispatches the events.
* - 最后 `EventPluginHub` 会分发/触发这些事件
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/

这里暂时还有很多细节没有被理解:

  • 合成事件概念 && 事件定义的数据结构
  • EventPluginHub && EventPluginRegistry 联系

合成事件 && 其数据结构

React定义了合成事件,它在顶层API上,对不同浏览器进行了兼容处理。它里面和原生事件一样有preventDefault && preventDefault

这里仅就最简单的SyntheticEvent做分析,其他的鼠标、滚动、拖曳之类的合成事件都是对它的拓展、继承。

这个合成事件的定义主要是三个地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var EventInterface = {
type: null,
target: null,
// currentTarget is set when dispatching; no use in copying it here
currentTarget: emptyFunction.thatReturnsNull,
eventPhase: null,
bubbles: null,
cancelable: null,
timeStamp: function(event) {
return event.timeStamp || Date.now();
},
defaultPrevented: null,
isTrusted: null,
};
function SyntheticEvent(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 定义位置一
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;

var Interface = this.constructor.Interface;
// 定义位置二 配合EventInterface看
for (var propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
var normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
if (propName === 'target') {
this.target = nativeEventTarget;
} else {
this[propName] = nativeEvent[propName];
}
}
}
// 略
return this;
}
// 定义位置三
Object.assign(SyntheticEvent.prototype, {
preventDefault: function() { },
stopPropagation: function() { },
persist: function() { },
isPersistent: emptyFunction.thatReturnsFalse,
destructor: function() { },
});

整体的属性、方法如下表。最有可能影响理解的可能是对原生event对象属性的基础上。仔细看看可以看到合成事件大部分都是在模拟原生Event对象的属性和方法。

属性、方法名 类型 说明
dispatchConfig
_targetInst ReactDomComponent实例或其他实例 ReactNode实例
nativeEvent DOMEvent 原生event对象
type string type
target DOMEventTarget target
currentTarget DOMEventTarget currentTarget
eventPhase number event.eventPhase
bubbles boolean bubbles
cancelable boolean cancelable
timeStamp number timeStamp
defaultPrevented boolean defaultPrevented
isTrusted boolean isTrusted
preventDefault Function 阻止浏览器默认行为
stopPropagation Function 停止冒泡
persist Function 阻止将其放入对象池 保持其引用
isPersistent boolean 检查是否应将此事件释放回对象池中
destructor Function PooledClass会用到它

这里dispatchConfig的大致结构如下, 关于这个数据结构SimpleEventPlugin.jseventTypes变量有注释和代码定义。

1
2
3
4
5
6
7
{
phasedRegistrationNames: {
bubbled: 'onClick',
captured: 'onClickCapture',
},
dependencies: ['topClick']
}

简单来说,onClick注册的事件会在冒泡环节触发,如果要在捕获环节触发,那么使用onClickCapture进行注册。这里phasedRegistrationNames还是很容易理解。而dependencies则是被依赖的top事件组成的数组,在冒泡捕获触发环节所有的事件诸如提到的onClick、onClickCapture最后都会被转换为topClick这样的形式进行触发。

EventPluginHub && EventPluginRegistry 联系

这里EventPluginHub是建立在EventPluginRegistry上的,从理解上,个人认为EventPluginRegistry实际上是处理的依赖注入的事情。他将plugin名称和本体保存起来,以供EventPluginHub方便调用。

三张图

想了一下,虽然自己最后分析了这么多,但是太过于重视细枝末节其实很难把握宏观上的处理方式。这源码就算读破了天,最终如果没有建立起体系其实没有什么卵用,所以这里画张图。

不过合成事件的各种继承,其实应该也值得很好研究一下,这里暂时不去细读这一块了。

reac 事件保存

react事件分发

react-event-system