浅谈React事件系统-源码解读

浅谈React事件系统主要分为两篇:

浅谈React事件系统-事件绑定
浅谈React事件系统-源码解读

准备工作

  • 去github下载一份源码react v16.2.0
  • 下载一个好用的编辑器, 支持快速定位到当前function的引用

    概念

在理解React中如何实现一个事件之前, 需要声明几个概念:

  1. React中几乎所有的事件都是代理到document监听
  2. 事件是由统一的分发函数dispatchEvent进行分发
  3. 事件对象(event)是合成对象(SyntheticEvent), 是原生的event对象的超集

事件处理步骤

React的事件系统主要分为四部曲:

  1. 事件绑定: 在初始化组件时, 会根据当前组件传入的事件属性, 在document上绑定对应的事件及其依赖的事件

  2. 事件分发: 页面组件触发的事件根据冒泡机制, 最后会被document上的listener捕获, 之后统一由dispatchEvent进行分发, 从触发事件的节点往上递归其祖先节点, 把事件分发给它们

  3. 事件合成: 当节点拿到一个事件之后, 首先要进行根据事件的类型, 进行事件的合成, React有个基础的SyntheticEvent对象, 事件合成就是根据事件类型给这个对象添加该事件才有的属性

  4. 事件回调函数执行: 获取Fiber(虚拟dom)中的事件回调函数, 执行它

源代码

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
引用源码里面的一个图来描述React的事件系统
/*
* 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
*/

事件绑定:

初始化一个组件的会调用 ensureListeningTo 给document对象绑定事件.
比如一个子组件绑定了一个onChange事件,
根据事件的冒泡机制, 那么document对象就要绑定一个onChange事件,
下面的代码就是根据不同的标签类型, 给document绑定不同的事件

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
// react-dom/src/client/ReactDOMFiberComponent
export function setInitialProperties(){
switch (tag) {
case 'iframe':
...
case 'select':
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
props = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
trapBubbledEvent('topInvalid', 'invalid', domElement);
ensureListeningTo(rootContainerElement, 'onChange');
break;
...
default:
props = rawProps;
}
}

// 这段代码主要就是给document绑定需要监听的事件
function ensureListeningTo(rootContainerElement, registrationName) {
var isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
var doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}

ensureListeningTo 最终是调用listenTo方法, listenTo主要作用是解决事件的依赖问题, 比如
onMouseEnter实际是依赖于 ‘topMouseOut’, ‘topMouseOver’两个事件, 所以document也要注册这两个事件

最终都是调用 trapCapturedEvent 或者 trapBubbledEvent 两个方法来进行实际的事件注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// react-dom/src/event/ReactBrowserEventEmitter
export function listenTo(registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
var dependencies = registrationNameDependencies[registrationName];

for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (dependency === 'topScroll') {
trapCapturedEvent('topScroll', 'scroll', mountAt);
...
...
} else if (topLevelTypes.hasOwnProperty(dependency)) {
trapBubbledEvent(dependency, topLevelTypes[dependency], mountAt);
}
isListening[dependency] = true;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// react-dom/src/event/ReactDOMEventListener
export function trapBubbledEvent(topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element,
handlerBaseName,
dispatchEvent.bind(null, topLevelType),
);
// EventListener.listen 可以类似于下面的代码, 但是做了一些平台上的兼容
// react有个merge request: Remove EventListener fbjs utility
// 就是直接用下面addEventListener的替换EventListener.listen, 但是拒绝了
element.addEventListener(
handlerBaseName,
dispatchEvent.bind(null, topLevelType),
true,
);
}

这时候, 已经完成第一个步骤: 事件绑定, 下面看一下事件是如何分发的

事件分发

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
// react-dom/src/event/ReactDOMEventListener
export function dispatchEvent(topLevelType, nativeEvent) {
var nativeEventTarget = getEventTarget(nativeEvent);
var targetInst = getClosestInstanceFromNode(nativeEventTarget);

// 生成一个BookKeeping的对象,包含 事件类型, 原生事件, 触发事件的target的实例
var bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
);

try {

// 下面的代码等同于 handleTopLevelImpl(bookKeeping), 后面可能会拓展batchedUpdate方法
// 目前只是预留了一个借口, handleTopLevelImpl里面有个for循环拿出来才叫 batchedUpdate吧
batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}


function handleTopLevelImpl(bookKeeping) {
var targetInst = bookKeeping.targetInst;
var ancestor = targetInst;
// 这里要做遍历触发事件的虚拟dom节点,以及该节点的祖先节点, 存到ancestors里面去
// 这个地方要把祖先的Fiber对象(virtual dom)存起来, 因为在_handleTopLevel可能会改变Fiber树(virtual dom tree)结构
do {
if (!ancestor) {
bookKeeping.ancestors.push(ancestor);
break;
}
var root = findRootContainerNode(ancestor);
if (!root) {
break;
}
bookKeeping.ancestors.push(ancestor);
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);

// 遍历 ancestors祖先节点, 调用 _handleTopLevel 方法去处理事件
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
_handleTopLevel(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent),
);
}
}

到这里, 假设有一个节点被点击了, 触发了onClick事件一直冒泡到document, document监听到这个事件, 触发 dispatchEvent方法, 遍历触发事件节点的祖先.

现在, 一个事件已经被dispatch了. 下面就是要进入到处理具体的某个事件的逻辑了

事件合成与处理 Mixin

事件合成与处理 Mixin: handleTopLevel 主要有三个逻辑

  1. 构造合成的事件
  2. 把合成事件push到 eventQueue
  3. processEventQueue处理eventQueue里面的事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// event/ReactEventEmitterMixin.js
export function handleTopLevel(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 合成事件
var events = extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 把合成的事件合并到事件队列里面
enqueueEvents(events);
// 处理事件队列里面的事件
processEventQueue(false);
}

事件合成

合成事件(SyntheticEvent)

React的事件系统有很多种事件合成的插件(Plugin), 这些插件就是根据不同的事件类型, 给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
export function extractEvents(
topLevelType: string,
targetInst: Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
) {
var events;
for (var i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
var possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
// 这里的extractedEvents 就是一个 合成事件(SyntheticEvent)对象
var extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
// accumulateInto这个方法实际上就是拼接两个数组
// 把每个插件合成的事件压到events数组里面
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}

主要的Plugin:

  1. SimpleEventPlugin
  2. TapEventPlugin
  3. Enter/LeavePlugin

先分析一下最基础的 simpleEventPlugin, 每个PluginModule都会有一个extractEvents对外的接口, 它主要的工作就是根据事件类型, 给Event对象添加一些数据特有的属性, 比如 MouseEvent就会加上 screenX, screenY, clientX, clientY 等等属性. 最终返回的是一个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
var SimpleEventPlugin: PluginModule<MouseEvent> = {
eventTypes: eventTypes,

extractEvents: function(
topLevelType: TopLevelTypes,
targetInst: Fiber,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {

// 根据事件的类型, 给 SyntheticEvent 这个对象添加一些属性
var EventConstructor;
switch (topLevelType) {
...

case 'topScroll':
EventConstructor = SyntheticUIEvent;
break;
case 'topWheel':
EventConstructor = SyntheticWheelEvent;
break;
...
default:
EventConstructor = SyntheticEvent;
break;
}
var event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 事件的回调函数
accumulateTwoPhaseDispatches(event);
return event;
},
};

上面的 accumulateTwoPhaseDispatches(event) 主要的工作:

  1. 获取当前虚拟dom实例绑定的事件回调函数
  2. 把事件回调函数push到 event._dispatchListeners 里面
  3. 把当前的Fiber(virtual-dom)实例push到 event._dispatchInstances 里面

这三个步骤很关键.

1
2
3
4
5
6
7
8
9
10
11
function accumulateDirectionalDispatches(inst, phase, event) {
// 获取当前dom节点中某某事件回调函数, 比如 onClick={(event)=>{}}
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}

enqueueEvents和processingEventQueue

上面事件已经调用各种plugin, 生成好了一个 events事件列表
接下来进行:

  1. 把合成的events合并到 eventQueue事件队列中(eventQueue是一个公用的事件队列)
  2. 遍历 processingEventQueue中的每个事件, 调用 executeDispatchesAndReleaseTopLevel去执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function enqueueEvents(events) {
if (events) {
// 把合成的事件数组合并到eventQueue
eventQueue = accumulateInto(eventQueue, events);
}
}

export function processEventQueue() {
// 这里在处理队列之前, 把eventQueue 设置为null
// 这样子当处理事件的时候, eventQueue也能继续接受新的事件
var processingEventQueue = eventQueue;
eventQueue = null;

// 遍历 processingEventQueue中的每个事件, 调用 executeDispatchesAndReleaseTopLevel去执行
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}

单个事件处理单元

executeDispatchesAndReleaseTopLevel里面主要是调用 executeDispatchesAndRelease这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var executeDispatchesAndRelease = function(
event: ReactSyntheticEvent,
simulated: boolean,
) {
if (event) {
// 按照顺序去依次执行回调函数, 遍历event._dispatchListeners并执行回调函数
executeDispatchesInOrder(event, simulated);

// React为了节省event对象频繁创建与销毁带来的性能损耗, 特地搞了一个对象池的概念
// 当执行完回调函数之后, 就回收这个SyntheticEvent对象
// 对象池在我们平常有频繁的对象创建销毁的场景中, 可以好好学习一下
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};

遍历event._dispatchListeners

下面的这个方法就是 遍历event._dispatchListeners并执行回调函数

  1. for循环遍历_dispatchListeners(回调函数列表)
  2. 如果isPropagationStopped为 true,就停止冒泡事件的执行
  3. 执行单个回调函数
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
export function executeDispatchesInOrder(event, simulated) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;

if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {

// 如果isPropagationStopped为 true
// 就不继续执行dispatchListeners里面其他回调函数了
// 这个地方是否就是react实现与原生事件一样的 事件停止冒泡传递, 待确定...
if (event.isPropagationStopped()) {
break;
}
// 去执行单个回调函数
executeDispatch(
event,
simulated,
dispatchListeners[i],
dispatchInstances[i],
);
}
// 处理一下 dispatchListeners 只有一个元素的情况, 目前看来大部分情况都是这种
} else if (dispatchListeners) {
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
// 依旧要重新初始化一下_dispatchListeners, _dispatchInstances, 因为event是要被回收的
event._dispatchListeners = null;
event._dispatchInstances = null;
}

执行单个回调函数

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
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
// currentTarget 是在这里加上的. 这里也直接打脸了最初我 '发现的React的BUG'
event.currentTarget = getNodeFromInstance(inst);
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
type,
listener,
undefined,
event,
);
event.currentTarget = null;
}

let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
ReactErrorUtils._hasCaughtError = false;
ReactErrorUtils._caughtError = null;
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
// 这里就真正的执行回调函数了
func.apply(context, funcArgs);
} catch (error) {
ReactErrorUtils._caughtError = error;
ReactErrorUtils._hasCaughtError = true;
}
};

看到这里, 也就完整的过了一下React的事件触发到执行的过程了.

总结:

关于性能: 在看源码之前, 在网上看过各种分析React事件系统的文章, 很多人的关注点是在于事件系统的性能多么好, 确实, React通过事件代理与分发的机制, 配合Fiber架构, 在提高事件绑定效率的同时, 也能高效的完成事件的分发处理, 在代码中, 随处可见的是队列, 从事件分发开始, 就是各种push队列, 执行队列, 保证了事件系统的稳定高效运行

关于跨平台: 对于各种终端而言, 只要还没有脱离显示屏这个载体, 交互过程中的各种事件的模型其实都不会差太多. 从React的事件系统中, 一个感受就是React正在逐步的消除平台之间的差异, 各个平台都有自己事件系统, 而React作为应用层框架, 实现自己的事件系统, 通过EventPlugin的方式来丰富Event对象, 用户在React体系下开发, 不管什么平台浏览器绑定一个点击事件只需要onClick就好了, 如果说Jquery的事件消除了浏览器之间的差异, 那么React的事件系统就是消除了平台之间的差异. 层次高了那么一丢丢

回过头再看React整个体系, 从React-dom, React, React-reconciler, Event, React-CS-Render, 串起来, 也恰恰印证了React官网首页的那句slogan: A JavaScript library for building user interfaces