React事件系统

挺久没有写博客了,最近一直在研究React的源码,对整个流程有了大概的了解,这里先总结下React事件系统的实现,之后还会对Fiber架构有个总结。

DOM原生事件

1
2
3
<div>
<button>click</button>
</div>

我们知道,当点击button时,会生成一个事件对象,经捕获(capture)document -> div -> button,冒泡(bubble)button -> div -> document,如果路径上的元素在对应阶段注册了回调,则执行回调,其中可能会preventDefault或stopPropagation,这个过程需要兼容不同的浏览器。

如果每个元素都注册回调,对性能的影响会很大,而解决的方法就是事件委托(event delegation)。

React事件系统主要解决的就是兼容问题和事件委托。

合成事件

出于兼容性考虑,React有自己实现的合成事件(SyntheticEvent),这也是React事件系统的基础。

重要属性

nativeEvent: 对应的原生事件

_targetInst: target对应的fiber

dispatchConfig: 该topLevel(topClick)对应各阶段的属性(onClick, onClickCaptured)

_dispatchListeners: 按事件流抓取的回调序列

_dispatchInstances: 回调对应的fiber序列

扩展性

DOM的事件对象中,根据事件类型,属性中有部分是共有的,比如target、type;其他是特有的,如MouseEvent的clientX等。为了可扩展性,React实现了类的Interface,当实例化的时候,会根据Interface把nativeEvent对应的属性赋值到实例里,并提供了extend方法来扩展Interface,实现继承。

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
SyntheticEvent.Interface = {
type: null,
defaultPrevented: null,
// ...
}

function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {
// ...
const Interface = this.constructor.Interface;
for (const propName in Interface) {
// ...
this[propName] = nativeEvent[propName];
}
}

SyntheticEvent.extend = function(Interface) {
const Super = this;

const E = function() {};
E.prototype = Super.prototype;
const prototype = new E();
// 类似 prototype = Object.create(Super.prototype)
function Class() {
return Super.apply(this, arguments);
}
Object.assign(prototype, Class.prototype);
Class.prototype = prototype;
Class.prototype.constructor = Class;
// 寄生组合式继承
Class.Interface = Object.assign({}, Super.Interface, Interface);
Class.extend = Super.extend;

return Class;
};

SyntheticUIEvent = SyntheticEvent.extend({
view: null,
// ...
})
SyntheticMouseEvent = SyntheticUIEvent.extend({
clientX: null,
// ...
})

原型方法

实现了兼容的preventDefault和stopPropagation,通过自定义的isPropagationStopped决定是否执行之后的回调。

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
Object.assign(SyntheticEvent.prototype, {
preventDefault: function() {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}

if (event.preventDefault) {
event.preventDefault();
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
},

stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}

if (event.stopPropagation) {
event.stopPropagation();
} else if (typeof event.cancelBubble !== 'unknown') {
event.cancelBubble = true;
}

this.isPropagationStopped = emptyFunction.thatReturnsTrue;
},

对象池

为了减少GC负担,合成事件使用了对象池,新建对象时会从对象池中取实例再赋值,使用后属性都重置再放入池中。

注册事件

React中的事件回调不绑定在实际的DOM元素中,我们知道React提供了更高的一层Fiber抽象,而回调就包含在Fiber的props中(如{onClick: f()})。当在根据Fiber初始化DOM元素(setInitialDOMProperties)时,如果发现有prop为onClick,则会将dispatchEvent.bind(null, "topClick")注册到document,使document开始监听click事件。

需要注意的是,document只起监听分发的作用,本身与回调是解耦的。监听到click事件后,在dispatchEvent的时候才开始抓取对应的回调,回调始终与DOM元素抽象出的fiber绑定,这样fiber消失时,回调也会消失。这也符合React的设计原则,即在消耗大的DOM元素上抽象出轻量化的fiber,以提高最终的性能。

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 trapBubbledEvent(topLevelType, baseName, element) {
if (!element) return
EventListener.listen(element, baseName, dispatchEvent.bind(null, topLevelType))
}

var EventListener = {
listen(target, eventType, callback) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, false)
return {
remove() {
target.removeEventListener(eventType, callback, false)
}
}
} else if (target.attachEvent) {
target.attachEvent("on" + eventType, callback)
return {
remove() {
target.detachEvent("on" + eventType, callback)
}
}
}
},
// ...
}

抓取事件

当document在冒泡阶段拿到nativeEvent,实际上事件流已经接近结束了。而React模拟DOM实现了自己的事件流来抓取事件并依次执行回调。

针对不同的事件,React有不同的plugin来得到回调。最普遍的SimplePlugin先得到从target到React容器的路径中DOM元素对应的fiber序列,再模拟捕获、冒泡进行两次遍历,分别寻找onClickCaptured, onClick属性,将得到的回调放入合成事件的_dispatchListeners中。

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
const SimpleEventPlugin = {
extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var dispatchConfig = topLevelTypeToDispatchConfig[topLevelType]
if (!dispatchConfig) return null
var EventConstructor
switch (topLevelType) {
case "topClick":
EventConstructor = SyntheticMouseEvent
break;
default:
break;
}
// pooled
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget)
traverseTwoPhase(targetInst, event)
return event
}
}

// 第一遍从上往下找onClickCaptured,第二遍从下往上找onClick
function traverseTwoPhase(inst, event) {
var path = [] // button fiber, div fiber
while (inst) {
if (inst.tag === HostComponent)
path.push(inst)
inst = inst.return
}
var i, len = path.length
for (i = len - 1; i >= 0; i--) {
accumulateDispatches(path[i], event, "captured")
}
for (i = 0; i < len; i++) {
accumulateDispatches(path[i], event, "bubbled")
}
}

// 找到phase对应的registrationName,找inst对应的prop
function accumulateDispatches(inst, event, phase) {
var registrationName = event.dispatchConfig.phasedRegistrationNames[phase]
// "onClick" "onClickCaptured"
var listener = getListener(inst, registrationName)
// 得到fiber对应属性名的回调
if (listener) {
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener)
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst)
}
}

执行回调

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
// simulated用于测试,这里意义不大
function executeDispatchesInOrder(event, simulated) {
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
// 如果之前的阶段执行了e.stopPropagation(),之后的回调就不会再执行
if (event.isPropagationStopped()) {
break;
}
executeDispatch(
event,
simulated,
dispatchListeners[i],
dispatchInstances[i],
);
}
} else if (dispatchListeners) {
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}

// 简化的执行单一回调
function executeDispatch(event, simulated, listener, inst) {
event.currentTarget = inst.stateNode; // 从fiber得到对应的DOM元素
listener.call(undefined, event)
// 从这里可以看出,如果没有特定bind或箭头函数,this为undefined
event.currentTarget = null;
}

补充

除了SimpleEventPlugin,React中还有其他Plugin来处理不同的事件,比如React针对input/select/textarea实现的特殊的onChange,需要ChangeEventPlugin来处理。另外还有特殊的topLevelType,这是对于浏览器特定事件的处理,比如transitionEnd, webkitTransitionEnd, MozTransitionEnd和oTransitionEnd都会集合为topAnimationEnd。

参考

  1. The React and React Native Event System Explained: A Harmonious Coexistence

  2. React源码

推荐文章