Que's Blog

WebFrontEnd Development

0%

React事件体系设计

源码结构

关于事件的源码,主要分布在3个地方。

第一个,src/renderers/dom/client/eventPlugins/

第二个,src/renderers/dom/client/syntheticEvents/

第三个,src/renderers/shared/stack/event/

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
src/renderers/dom/client/eventPlugins/
├── BeforeInputEventPlugin.js
├── ChangeEventPlugin.js
├── DefaultEventPluginOrder.js
├── EnterLeaveEventPlugin.js
├── FallbackCompositionState.js
├── SelectEventPlugin.js
├── SimpleEventPlugin.js
├── TapEventPlugin.js

src/renderers/dom/client/syntheticEvents/
├── SyntheticAnimationEvent.js
├── SyntheticClipboardEvent.js
├── SyntheticCompositionEvent.js
├── SyntheticDragEvent.js
├── SyntheticFocusEvent.js
├── SyntheticInputEvent.js
├── SyntheticKeyboardEvent.js
├── SyntheticMouseEvent.js
├── SyntheticTouchEvent.js
├── SyntheticTransitionEvent.js
├── SyntheticUIEvent.js
├── SyntheticWheelEvent.js

src/renderers/shared/stack/event
├── EventConstants.js
├── EventPluginHub.js
├── EventPluginRegistry.js
├── EventPluginUtils.js
├── EventPropagators.js
├── PluginModuleType.js
├── ReactSyntheticEventType.js
├── SyntheticEvent.js
└── eventPlugins
├── ResponderEventPlugin.js
├── ResponderSyntheticEvent.js
├── ResponderTouchHistoryStore.js
├── TouchHistoryMath.js

合成事件设计

这里首先得看看合成事件是怎样设计的。

src/renderers/dom/client/syntheticEvents/可以看到,诸多的合成事件,基本都是走的以下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SyntheticInputEvent(
dispatchConfig,
dispatchMarker,
nativeEvent,
nativeEventTarget,
) {
return SyntheticEvent.call(
this,
dispatchConfig,
dispatchMarker,
nativeEvent,
nativeEventTarget,
);
}
SyntheticEvent.augmentClass(SyntheticInputEvent, InputEventInterface);
module.exports = SyntheticInputEvent;

这里主要就是两个调用SyntheticEvent.call(args)SyntheticEvent.augmentClass

这里就看看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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
SyntheticEvent.augmentClass = function(Class, Interface) {
var Super = this; // this指向SyntheticEvent 此处augmentClass作为SyntheticEvent方法

var E = function() {}; // 建立一个空函数
E.prototype = Super.prototype; // 为空行书设定SyntheticEvent的原型
var prototype = new E(); // new这个函数,新的函数原型链指向了SyntheticEvent的原型

// 将Class的(此场景是SyntheticInputEvent)原型合并到new出来的新函数上
Object.assign(prototype, Class.prototype);
// 设定Class原型 并重置constructor为自身
// 相当于 Class.prototype = prototype();Class.prototype.constructor = Class;
// 但是这个方法可以减少一层原型链调用
Class.prototype = prototype;
Class.prototype.constructor = Class;

// 其实也就是设定了this.constructor.Interface;
// Interface的值 此处是SyntheticEvent.Interface 和 InputEventInterface的并集
// 这里可以注意到第一个参数是{}, SyntheticEvent.Interface没有被污染
Class.Interface = Object.assign({}, Super.Interface, Interface);
Class.augmentClass = Super.augmentClass;

PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler);
};

function SyntheticEvent(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 基于上面SyntheticInputEvent的例子,这里this指向SyntheticInputEvent
// 具体指向还得看SyntheticEvent.call第一个参数传入的context
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;

// 此处是SyntheticEvent.Interface 和 InputEventInterface的并集
var Interface = this.constructor.Interface;
for (var propName in Interface) {
// 如果propName在原型上 跳过后面代码 进行下一个循环
if (!Interface.hasOwnProperty(propName)) {
continue;
}
var normalize = Interface[propName];
// 这里后面的代码可以有这样一个论断:
// Interface上的属性有两种,一种是属性,值统一为null|undefined之类
// 一种是方法,这个方法接受event作为参数
// 最后this被根据Interface统一赋值完毕
// normalize如果是函数,this[propName]就接受事件运行返回值
// 否则normalize就是值,this[propName] = nativeEvent[propName]
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
if (propName === 'target') {
this.target = nativeEventTarget;
} else {
this[propName] = nativeEvent[propName];
}
}
}
// https://developer.mozilla.org/zh-CN/docs/Web/API/Event/defaultPrevented
// 返回一个布尔值,表明当前事件是否调用了 event.preventDefault()方法。
var defaultPrevented = nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false; // 旧浏览器兼容
if (defaultPrevented) {
this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
} else {
this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
}
this.isPropagationStopped = emptyFunction.thatReturnsFalse;
return this;
}

这里执行的入口是SyntheticEvent.augmentClass,后面跟着执行SyntheticEvent.call(arg),具体逻辑都详细列出来了。

这两个主要做的事情检出来说说:

  • 合并SyntheticEvent和SyntheticInputEvent的两者的Interface,对SyntheticInputEvent按需赋值
  • SyntheticInputEvent继承SyntheticEvent原型链
  • SyntheticInputEvent三个属性
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
69
70
71
72
73
74
SyntheticEvent.augmentClass = function(Class, Interface) {
var Super = this; // this指向SyntheticEvent 此处augmentClass作为SyntheticEvent方法

var E = function() {}; // 建立一个空函数
E.prototype = Super.prototype; // 为空行书设定SyntheticEvent的原型
var prototype = new E(); // new这个函数,新的函数原型链指向了SyntheticEvent的原型

// 将Class的(此场景是SyntheticInputEvent)原型合并到new出来的新函数上
Object.assign(prototype, Class.prototype);
// 设定Class原型 并重置constructor为自身
// 相当于 Class.prototype = prototype();Class.prototype.constructor = Class;
// 但是这个方法可以减少一层原型链调用
Class.prototype = prototype;
Class.prototype.constructor = Class;

// 其实也就是设定了this.constructor.Interface;
// Interface的值 此处是SyntheticEvent.Interface 和 InputEventInterface的并集
// 这里可以注意到第一个参数是{}, SyntheticEvent.Interface没有被污染
Class.Interface = Object.assign({}, Super.Interface, Interface);
Class.augmentClass = Super.augmentClass;

PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler);
};

function SyntheticEvent(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 基于上面SyntheticInputEvent的例子,这里this指向SyntheticInputEvent
// 具体指向还得看SyntheticEvent.call第一个参数传入的context
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;

// 此处是SyntheticEvent.Interface 和 InputEventInterface的并集
var Interface = this.constructor.Interface;
for (var propName in Interface) {
// 如果propName在原型上 跳过后面代码 进行下一个循环
if (!Interface.hasOwnProperty(propName)) {
continue;
}
var normalize = Interface[propName];
// 这里后面的代码可以有这样一个论断:
// Interface上的属性有两种,一种是属性,值统一为null|undefined之类
// 一种是方法,这个方法接受event作为参数
// 最后this被根据Interface统一赋值完毕
// normalize如果是函数,this[propName]就接受事件运行返回值
// 否则normalize就是值,this[propName] = nativeEvent[propName]
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
if (propName === 'target') {
this.target = nativeEventTarget;
} else {
this[propName] = nativeEvent[propName];
}
}
}
// https://developer.mozilla.org/zh-CN/docs/Web/API/Event/defaultPrevented
// 返回一个布尔值,表明当前事件是否调用了 event.preventDefault()方法。
var defaultPrevented = nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false; // 旧浏览器兼容
if (defaultPrevented) {
this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
} else {
this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
}
this.isPropagationStopped = emptyFunction.thatReturnsFalse;
return this;
}

这里执行的入口是SyntheticEvent.augmentClass,后面跟着执行SyntheticEvent.call(arg),具体逻辑都详细列出来了。

这两个主要做的事情检出来说说:

  • 合并SyntheticEvent和SyntheticInputEvent的两者的Interface,对SyntheticInputEvent按需赋值
  • SyntheticInputEvent继承SyntheticEvent原型链
  • SyntheticInputEvent三个属性dispatchConfig|_targetInst|nativeEvent的赋值
  • isDefaultPrevented和isPropagationStopped赋值

最后事情捡回来说SyntheticInputEvent实质上还是SyntheticEvent函数的的返回值。所以这里事情的本质还是对SyntheticEvent的继承和扩展。

关于这个继承,主要是基于新的合成类型的Interface和event事件进行处理的。

合成事件 和 原生事件 区别

这里反思一下,合成事件和nativeEvent的区别在哪呢?这里根据源码总结一下:

  • 添加了三个属性dispatchConfig|_targetInst|nativeEvent
  • Interface上设计了一些方法,这里赋值会根据函数进行赋值,这是一个设计模式,类似vue的computed。
  • defaultPrevented属性设为必有的布尔值, isPropagationStopped恒等于false

合成事件的继承关系

基于合成事件 和 原生事件和关系,那么不同合成事件的关系继承,就很有一些猜测的意思了。

这里可以直接推论到,继承的核心,就是那些Interface对象上的属性和方法。

下面根据标题做了一个树,需要配合目录层级看合成事件继承关系。

SyntheticEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,
};

SyntheticAnimationEvent

Extends SyntheticEvent

1
2
3
4
5
var AnimationEventInterface = {
animationName: null,
elapsedTime: null,
pseudoElement: null,
};

ClipboardEventInterface

Extends SyntheticEvent

1
2
3
4
5
6
7
var ClipboardEventInterface = {
clipboardData: function(event) {
return 'clipboardData' in event
? event.clipboardData
: window.clipboardData;
},
};

SyntheticCompositionEvent

Extends SyntheticEvent

1
2
3
var CompositionEventInterface = {
data: null,
};

SyntheticInputEvent

Extends SyntheticEvent

1
2
3
var InputEventInterface = {
data: null,
};

SyntheticTransitionEvent

Extends SyntheticEvent

1
2
3
4
5
var TransitionEventInterface = {
propertyName: null,
elapsedTime: null,
pseudoElement: null,
};

ResponderSyntheticEvent

Extends SyntheticEvent

1
2
3
4
5
var ResponderEventInterface = {
touchHistory: function(nativeEvent) {
return null; // Actually doesn't even look at the native event.
},
};

SyntheticUIEvent

Extends SyntheticEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var UIEventInterface = {
view: function(event) {
if (event.view) {
return event.view;
}
var target = getEventTarget(event);
if (target.window === target) {
return target;
}

var doc = target.ownerDocument;
if (doc) {
return doc.defaultView || doc.parentWindow;
} else {
return window;
}
},
detail: function(event) {
return event.detail || 0;
},
};
SyntheticTouchEvent
1
2
3
4
5
6
7
8
9
10
var TouchEventInterface = {
touches: null,
targetTouches: null,
changedTouches: null,
altKey: null,
metaKey: null,
ctrlKey: null,
shiftKey: null,
getModifierState: getEventModifierState,
};
SyntheticFocusEvent
1
2
3
var FocusEventInterface = {
relatedTarget: null,
};
SyntheticKeyboardEvent
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
var KeyboardEventInterface = {
key: getEventKey,
location: null,
ctrlKey: null,
shiftKey: null,
altKey: null,
metaKey: null,
repeat: null,
locale: null,
getModifierState: getEventModifierState,
charCode: function(event) {
if (event.type === 'keypress') {
return getEventCharCode(event);
}
return 0;
},
keyCode: function(event) {
if (event.type === 'keydown' || event.type === 'keyup') {
return event.keyCode;
}
return 0;
},
which: function(event) {
if (event.type === 'keypress') {
return getEventCharCode(event);
}
if (event.type === 'keydown' || event.type === 'keyup') {
return event.keyCode;
}
return 0;
},
};
SyntheticMouseEvent
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 MouseEventInterface = {
screenX: null,
screenY: null,
clientX: null,
clientY: null,
ctrlKey: null,
shiftKey: null,
altKey: null,
metaKey: null,
getModifierState: getEventModifierState,
button: function(event) {
var button = event.button;
if ('which' in event) {
return button;
}
return button === 2 ? 2 : button === 4 ? 1 : 0;
},
buttons: null,
relatedTarget: function(event) {
return (
event.relatedTarget ||
(event.fromElement === event.srcElement
? event.toElement
: event.fromElement)
);
},
pageX: function(event) {
return 'pageX' in event
? event.pageX
: event.clientX + ViewportMetrics.currentScrollLeft;
},
pageY: function(event) {
return 'pageY' in event
? event.pageY
: event.clientY + ViewportMetrics.currentScrollTop;
},
};
SyntheticDragEvent
1
2
3
var DragEventInterface = {
dataTransfer: null,
};
SyntheticWheelEvent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var WheelEventInterface = {
deltaX: function(event) {
return 'deltaX' in event
? event.deltaX
: 'wheelDeltaX' in event ? -event.wheelDeltaX : 0;
},
deltaY: function(event) {
return 'deltaY' in event
? event.deltaY
: 'wheelDeltaY' in event
? -event.wheelDeltaY
: 'wheelDelta' in event ? -event.wheelDelta : 0;
},
deltaZ: null,
deltaMode: null,
};

EventPlugin

XXXPlugins是对合成事件的调配。根据不同的事件,它有一个方法可以专门返回合成事件实例。

它的结构大致是这样的:

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
type DispatchConfig = {
dependencies: Array<string>,
phasedRegistrationNames?: {
bubbled: string,
captured: string,
},
registrationName?: string,
};
type EventTypes = {[key: string]: DispatchConfig};
export type PluginModule<NativeEvent> = {
eventTypes: EventTypes,
extractEvents: (
topLevelType: string,
targetInst: ReactInstance,
nativeTarget: NativeEvent,
nativeEventTarget: EventTarget,
) => null | ReactSyntheticEvent,
didPutListener?: (
inst: ReactInstance,
registrationName: string,
listener: () => void,
) => void,
willDeleteListener?: (inst: ReactInstance, registrationName: string) => void,
tapMoveThreshold?: number,
};
type Plugins {
eventTypes: EventTypes;
extractEvents: (
topLevelType: TopLevelTypes,
targetInst: ReactInstance,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
) => PluginModule<NativeEvent>
}

其中extractEvents是一个重要API,它会返回一个合成事件实例。

这里就SimpleEventPlugin讲一讲。这是一个非常重要的Plugin。

它核心逻辑如下:

  1. 判断topLevelType的值,根据这个值返回一个合成事件生产函数
  • 如果是topAbort topCanPlay topCanPlayThrough topDurationChange topEmptied topEncrypted topEnded topError topInput topInvalid topLoad topLoadedData topLoadedMetadata topLoadStart topPause topPlay topPlaying topProgress topRateChange topReset topSeeked topSeeking topStalled topSubmit topSuspend topTimeUpdate topVolumeChange topWaiting之一,返回SyntheticEvent。

  • 如果是topKeyPress topClick返回null

  • 如果是topKeyUp返回SyntheticKeyboardEvent

  • 如果是topKeyDown topKeyUp返回SyntheticFocusEvent

  • 如果是topDoubleClick topMouseDown topMouseMove topMouseUp topMouseOut topMouseOver topContextMenu返回SyntheticMouseEvent

  • 如果是topDra topDragEn topDragEnte topDragExi topDragLeav topDragOve topDragStar topDrop返回SyntheticDragEvent

  • 如果是topTouchCancel topTouchEnd topTouchMove topTouchStart返回SyntheticTouchEvent

  • 如果是topAnimationEnd topAnimationIteration topAnimationStart返回SyntheticAnimationEvent

  • 如果是topTransitionEnd返回SyntheticTransitionEvent

  • 如果是topScroll返回SyntheticUIEvent

  • 如果是topWheel返回SyntheticWheelEvent

  • 如果是topCopy topCut topPaste返回SyntheticClipboardEvent

  1. 使用这个函数传入目标参数返回合成事件实例

  2. 执行EventPropagators.accumulateTwoPhaseDispatches(event)。这个函数在虚拟DOM上模拟事件捕获和冒泡,并将所有的实际回调都缓存到数组保存好,具体可以参见React Event#extractEvents,这里不涉及这个细节。但是当你从这个链接调出再返回时,务必记得event._dispatchListeners && event._dispatchInstances这两个变量从哪来。

EventPluginHub

在Plugin上一层,还有一个EventPluginHub的设计。

如果说Plugin是对合成事件的调配,那么EventPluginHub就是对Plugin的调配。它的结构大致如下,细节代码这里不放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var EventPluginHub = {
// 保存事件回调到`listenerBank[registrationName][key]`这个位置
putListener: function(inst, registrationName, listener) {},
// putListener的反向操作 从listenerBank[registrationName][key]读取事件回调
// 鼠标事件存在Prevent可能 如果被阻止了 那就返回null
getListener: function(inst, registrationName) { },
// 从listenerBank[registrationName]移除对应实例的registrationName类型的回调
deleteListener: function(inst, registrationName) { },
// 从listenerBank里面移除对应实例所有类型的回调
deleteAllListeners: function(inst) { },
// 遍历所有的Plugins(EventPluginRegistry.plugins),挨个执行每个Plugin上的extractEvents,如果有返回,进入模拟捕获和冒泡环节
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) { },
// 将获取到的合成事件放到processEventQueue以待执行
enqueueEvents: function(events) { },
// 调用processEventQueue里面所有的合成事件
processEventQueue: function(simulated) { },
};

EventPluginRegistry

上一小结提到了EventPluginRegistry.plugins,这里对这个进行一下分析。其主要结构是:

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
type DispatchConfig = {
dependencies: Array<string>,
phasedRegistrationNames?: {
bubbled: string,
captured: string,
},
registrationName?: string,
};
type EventTypes = {[key: string]: DispatchConfig};
type DomAttrEvent = Omit(DOMAttributes, 'children', 'dangerouslySetInnerHTML');
var EventPluginRegistry = {
plugins: Array<Plugins>;
eventNameDispatchConfigs: EventTypes;
// DOMAttributes参见@types/react
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L1240
registrationNameModules: {[key: keyof DomAttrEvent]: PluginModule};
registrationNameDependencies: {[key: keyof DomAttrEvent]: Array<keyof topLevelTypes>},
possibleRegistrationNames; // "onabort": "onAbort"|"onclick": "onClick"这种键值对
// 为eventPluginOrder赋值 这里值为固定值 以备后面为进行排序
// ["ResponderEventPlugin", "SimpleEventPlugin", "TapEventPlugin", "EnterLeaveEventPlugin", "ChangeEventPlugin", "SelectEventPlugin", "BeforeInputEventPlugin"]
injectEventPluginOrder: (injectedEventPluginOrder: EventPluginOrder) => void;
// 为namesToPlugins,设定一个Map结构 可以通过PluginName获取Plugin,并根据eventPluginOrder排序
injectEventPluginsByName: (injectedNamesToPlugins: NamesToPlugins) => void;
getPluginModuleForEvent: (event: ReactSyntheticEvent) => null | PluginModule<AnyNativeEvent>)
}

平心而论,这个EventPluginRegistry对象是一个极其庞大的对象。从运行栈里直接copy json(不含函数),就有72000+行之巨,大多数的dom上的事件props名称的兼容相关,在这里都有体现。

如此巨量的数据,配合Registry名称,这里是所有合成事件相关数据保存位置,以及各种原生事件和合成事件的对应映射、兼容映射。(但是这里不是事件回调保存位置所在)。

关于这个EventPluginRegistry,不是特别容易读,虽然代码还是很简单,但是从代码推导其作用,有点折腾,推荐还是直接看EventPluginRegistry-test.js测试文件。

这里核心API就两个injectEventPluginsByName, injectEventPluginOrder。其他的变量、函数要么是为了实现这个API,要么是为了测试这个API而声明的。这两个函数为EventPluginHub模块实现事件插件模块的加载。

这两个核心API使用时候是可以不在乎调用顺序的,只要调用时传入了有效参数,他们都会走一次排序流程,这个流程定义在recomputePluginOrdering,排序目标是EventPluginRegistry.plugins数组,排序依据是injectEventPluginOrder传入的参数。

这个recomputePluginOrdering函数值得一看。因为有提到phasedRegistrationNames的处理。这个函数里面执行了publishEventForPlugin,这个函数会遍历phasedRegistrationNames里面的bubbled&captured属性,然后调用publishRegistrationName。

这个函数核心逻辑如下:

1
2
3
EventPluginRegistry.registrationNameModules[registrationName] = pluginModule;
EventPluginRegistry.registrationNameDependencies[registrationName] =
pluginModule.eventTypes[eventName].dependencies;

也就是在对应的registrationNameModules[registrationName] && registrationNameDependencies[registrationName]赋值,改为对应Plugin的值和dependencies。

这可以视为一个瞬时变量,因为每次遇到同名的registrationName会被覆盖,这实际上也是加载模块的意义所在。

我们再看看当加载完毕后EventPluginHub,这个模块是如何用到的它们。

  • putListener。这里主要是判断EventPluginRegistry.registrationNameModules[registrationName]值存在,执行换个EventPlugin上的didPutListener,这个函数一般是一些兼容hack处理。
  • deleteListener。这是针对didPutListener的反向操作,执行的是这个EventPlugin上的willDeleteListener
  • deleteAllListeners,加强版的deleteListener,直接遍历执行全部EventPlugin的deleteListener。
  • extractEvents。遍历plugin模拟冒泡并执行。

这里还缺registrationNameDependencies[registrationName]的线索,再找找。在ReactBrowserEventEmitter.js#line254这里开始有引用。

我们忽略特殊分支topWheel、topScroll、topFocus、topBlur。通用性做法是:

1
2
3
4
5
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
dependency,
topEventMapping[dependency],
mountAt,
);

这里有很多细节,不过我们在ReactEvent Part1里面有很详细的说明。这里可以移步过去看看再回来看继续的。

看过Part1,思路就可以更加清晰了,因为这部分是事件触发过后,往后继续执行的过程。在Part1里面我们深入这个过程将他们串联起来,但是并没有架构起合成事件实现的宏观架构。

首先是ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent指向ReactEventListener.trapBubbledEvent。进而调用了EventListener.listen->target.addEventListener

也就是说,在这个链条中dependency进入了真实的DOM事件绑定环节。维系这个环节关键是addEventListener使用的click这样的事件名,而dependency是topClick这样的字符串数组。这里转换的Map结构是topEventMapping。

关于EventPluginRegistry我们到这一环大概可以结束了,我们带着疑问进入下一环节(后面会继续这块)。

关联

这里的关联是,指的是SyntheticEvent、EventPlugin、EventPluginHub、EventPluginRegistry、DispatchConfig之间的关联、定义、相互作用这些。

这里画个图看看整体关联。

image-20190712081047028

仔细观察这个图里面的脉络,可以看到DispatchConfig是一个核心环节,它构成了不同模块之间相互调用的基础

在EventPluginRegistry.getPluginModuleForEvent函数中有这样的细节:

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
getPluginModuleForEvent: function(
event: ReactSyntheticEvent,
): null | PluginModule<AnyNativeEvent> {
var dispatchConfig = event.dispatchConfig;
if (dispatchConfig.registrationName) {
return (
EventPluginRegistry.registrationNameModules[
dispatchConfig.registrationName
] || null
);
}
if (dispatchConfig.phasedRegistrationNames !== undefined) {
var {phasedRegistrationNames} = dispatchConfig;
for (var phase in phasedRegistrationNames) {
if (!phasedRegistrationNames.hasOwnProperty(phase)) {
continue;
}
var pluginModule =
EventPluginRegistry.registrationNameModules[
phasedRegistrationNames[phase]
];
if (pluginModule) {
return pluginModule;
}
}
}
return 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
26
27
28
29
30
31
32
33
it('should be able to get the plugin from synthetic events', () => {
var clickDispatchConfig = {
registrationName: 'onClick',
};
var magicDispatchConfig = {
phasedRegistrationNames: {
bubbled: 'onMagicBubble',
captured: 'onMagicCapture',
},
};
var OnePlugin = createPlugin({
eventTypes: {
click: clickDispatchConfig,
magic: magicDispatchConfig,
},
});

var clickEvent = {dispatchConfig: clickDispatchConfig};
var magicEvent = {dispatchConfig: magicDispatchConfig};

expect(EventPluginRegistry.getPluginModuleForEvent(clickEvent)).toBe(null);
expect(EventPluginRegistry.getPluginModuleForEvent(magicEvent)).toBe(null);

EventPluginRegistry.injectEventPluginsByName({one: OnePlugin});
EventPluginRegistry.injectEventPluginOrder(['one']);

expect(EventPluginRegistry.getPluginModuleForEvent(clickEvent)).toBe(
OnePlugin,
);
expect(EventPluginRegistry.getPluginModuleForEvent(magicEvent)).toBe(
OnePlugin,
);
});

虽然这个函数没有被源码正式引用过(仅在EventPluginRegistry-test.js使用),但是依然不妨碍我们去理解DispatchConfig。结合这个测试文件里面的内容,也可以对DispatchConfig做一些推论。

核心:DispatchConfig

然而你要懂DispatchConfig,就必须回过头重新看看它的定义:

1
2
3
4
5
6
7
8
type DispatchConfig = {
dependencies: Array<string>,
phasedRegistrationNames?: {
bubbled: string,
captured: string,
},
registrationName?: string,
};

然而光看定义还是不够,这里还有问题

  • registrationName,phasedRegistrationNames究竟长什么样呢?
  • 根据getPluginModuleForEvent函数有一个很明确的推导,这两个属性基本是二选一的,它们又有什么区别呢?
  • dependencies又是从哪来,有什么用?

首先registrationName是onChange|onClick这种形式的string,我们写jsx时候很常用。接着就是phasedRegistrationNames,它一般是这样的:

1
2
3
4
phasedRegistrationNames: {
bubbled: 'onClickBubble',
captured: 'onClickCapture',
},

其次,他们是什么关系呢? 实际上phasedRegistrationNames里面定义的是冒泡和捕获阶段的事件注册名称,dependencies这相对简单,就是依赖事件名的数组,比如这里就是[topClick], 而registrationName这是topClick。

这里再看看其他的一些例子:

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
// EnterLeaveEventPlugin
var eventTypes = {
mouseEnter: {
registrationName: 'onMouseEnter',
dependencies: ['topMouseOut', 'topMouseOver'],
},
mouseLeave: {
registrationName: 'onMouseLeave',
dependencies: ['topMouseOut', 'topMouseOver'],
},
};
// ChangeEventPlugin
var eventTypes = {
change: {
phasedRegistrationNames: {
bubbled: 'onChange',
captured: 'onChangeCapture',
},
dependencies: [
'topBlur',
'topChange',
'topClick',
'topFocus',
'topInput',
'topKeyDown',
'topKeyUp',
'topSelectionChange',
],
},
};

dependencies是都有的,通过topEventMapping[dependencie]获取事件名,它们参与了实际的事件绑定环节。

而通过phasedRegistrationNames || registrationName获取了对应EventPlugin。不过这里我们通过测试文件推论这个虽然不可能不准。但是最好还是找出真实的代码所在(虽然近乎前面已经提到了)。

这里核心代码就是publishEventForPlugin+publishRegistrationName。publishRegistrationName这里就不再提,上一小节已经给做了完善等解析。我们这里来讲讲之前被跳过的publishEventForPlugin。

在这个函数里面针对phasedRegistrationNames || registrationName做了分支处理。

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
function publishEventForPlugin(
dispatchConfig: DispatchConfig,
pluginModule: PluginModule<AnyNativeEvent>,
eventName: string,
): boolean {
EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig;
var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
if (phasedRegistrationNames) {
for (var phaseName in phasedRegistrationNames) {
if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
var phasedRegistrationName = phasedRegistrationNames[phaseName];
publishRegistrationName(
phasedRegistrationName,
pluginModule,
eventName,
);
}
}
return true;
} else if (dispatchConfig.registrationName) {
publishRegistrationName(
dispatchConfig.registrationName,
pluginModule,
eventName,
);
return true;
}
return false;
}

可以看到,核心他们最后都是直接执行了publishRegistrationName函数,这个函数之前有说,这也反应了,其实

registrationName & phasedRegistrationName是同一层面的东西。通过这两个变量,可以确定对应EventPlugin。这里测试文件里面有很明显的指向: EventPluginRegistry.registrationNameModules && EventPluginRegistry.registrationNameModules[phasedRegistrationNames[phase]]

然后的逻辑里面,这里可以确认dependencies是这里的核心。让我们继续刚才的疑问,当dependencies介入到了真实DOM的事件绑定这里,后面发生什么。

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),
);
},

关于EventListener.listen这段,我们无妨把它理解成:

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

这里显然可以看看了ReactEventListener.dispatchEvent.bind(null, topLevelType)。这里还是重申一次(Part1里面提到过),这里只是提前给topLevelType赋了值,后面一旦作为EventHandle引用,必定还会传入一个NativeEvent对象。所以两个参数最后都不会缺。

我们这里关注点放在bookKeeping上。它实质上是一个对象:

1
2
3
4
5
{
topLevelType,
nativeEvent,
ancestors = []
}

ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping)这个调用我们不管其细节了,如果想要了解这个细节,这里有两个地方可以做参考React ReactUpdatesreact render环节分析

这里只针对关键说明,它最终进行了handleTopLevelImpl(bookKeeping)调用。这个函数分析可以参见React-Event#handleTopLevelImpl

我们可以从这个分析里面看到。它调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
handleTopLevel: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 这里对event是一个合成事件对象,因为extractEvents可能返回null,数组里面也可能有null成员
var events = EventPluginHub.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
runEventQueueInBatch(events);
}

接下来还是基于之前的分析来分析这里的重点(如果你逻辑接不上,那么一定需要重新读Part1那些…)。

接下来是executeDispatchesAndRelease函数

1
2
3
4
5
6
7
8
9
var executeDispatchesAndRelease = function(event, simulated) {
if (event) {
EventPluginUtils.executeDispatchesInOrder(event, simulated);
// 持久化处理 如果不做特别处理 会被释放 后面就无法访问原生NativeEvent了。
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};

executeDispatchesInOrder函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function executeDispatchesInOrder(event, simulated) {
// 如果不知道这两个变量,EventPlugin小节可以重新看看
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
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;
}

executeDispatch函数:

1
2
3
4
5
6
7
8
9
10
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}

invokeGuardedCallback函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function invokeGuardedCallback<A>(
name: string,
func: (a: A) => void,
a: A,
): void {
try {
func(a);
} catch (x) {
if (caughtError === null) {
caughtError = x;
}
}
}

到了最后,整体的逻辑大致如此:

  • executeDispatchesInOrder函数从合成事件上获取_dispatchListeners回调函数
  • executeDispatch从合成事件上获取type,获取实质事件名称如click、change、error(获取的事件名称在开发环境下会被用到,生产没有,所以不做分析了)
  • invokeGuardedCallback则是直接调用上面获取的的回调,第一个参数传入合成事件(类似常规的EventHandle)。

Part1 && Part2的融合

Part1着重讲了绑定了触发,Part2则着重讲了体系。

现在这里需要一个最后的融合,将他们融汇贯通(当然这里还是需要先对Render&Update有印象)。

这里来个简单例子。

1
2
3
4
5
6
class Bar extends React.Component{
render () {
return <button onClick={() => console.log('click test button')}>test</button>
}
}
ReactDOM.render(document, Bar)

绑定路径

初始化绑定

这里对绑定从ReactDom.render开始算起。这一块详细细节都在React Render里面。

不过这里在mountComponentIntoNode这一小节有一个细节因为和纯粹的Render联系不大被略过了。

那就是这个函数里面的

1
2
3
4
5
6
7
8
var markup = ReactReconciler.mountComponent(
wrapperInstance,
transaction,
null,
ReactDOMContainerInfo(wrapperInstance, container),
context,
0 /* parentDebugID */,
);

这个mountComponent会一路从自定义组件到ReactDom组件的同名方法,最终会调动this._updateDOMProperties。这个函数事件绑定的关键。

更新逻辑绑定

更新逻辑可以看react生命周期分析

更新会从ReactReconciler.receiveComponent会一路从自定义组件到ReactDom组件的同名方法,然后走receiveComponent->updateComponent->_updateDOMProperties的路径回到和初始化render相同的逻辑上来。

绑定细节

这里需要看看_updateDOMProperties源码。这里遴选了事件部分的核心代码:

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
var registrationNameModules = EventPluginRegistry.registrationNameModules;
_updateDOMProperties: function(lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;
for (propKey in lastProps) {
if (registrationNameModules.hasOwnProperty(propKey)) {
if (lastProps[propKey]) { // 更新过程中移除旧的事件回调
deleteListener(this, propKey);
}
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = lastProps != null ? lastProps[propKey] : undefined; // 方便理解有改动
if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// EventPluginHub.putListener函数的事务包装 所有操作结束后重新绑定事件
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
// 移除对应propKey的旧元素事件 这里应该是为了确保不会被旧元素事件污染
deleteListener(this, propKey);
}
}
}
}
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,
});
}

★所以来说,这里可以看出EventPluginRegistry.registrationNameModules是onClick这些的一个Map数据,我们此时可以明白,为什么key是onClick、onChange这些。

这里对事件绑定的两个环节要有概念,它们都在enqueuePutListener函数里面:

  • listenTo调用了addEventListener绑定了事件到了document,这里绑定的回调函数是ReactEventListener.dispatchEvent.bind(null, topLevelType)
  • putListener把回调保存到了EventPluginHub.listenerBank

这个listenTo里面有些细节必须扯出来说说。

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
listenTo: function(registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
var dependencies =
EventPluginRegistry.registrationNameDependencies[registrationName];

for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (
!(isListening.hasOwnProperty(dependency) && isListening[dependency])
) {
if (dependency === 'topWheel') {
// 略
} else if (dependency === 'topScroll') {
// 略
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
// 略
} else if (topEventMapping.hasOwnProperty(dependency)) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
dependency, // 可能会取值onClick, onChange
topEventMapping[dependency], // 可能会取值click, change
mountAt, // 大概率取值document
);
}

isListening[dependency] = true;
}
}
},

trapBubbledEvent实际上我们之前说到过。这里核心是会调用到

1
2
3
4
5
document.addEventLinstener(
topEventMapping[dependency],
ReactEventListener.dispatchEvent.bind(null, topLevelType), // 这里是事件分发的核心
false
)

事件触发、分发

事件的触发、分发实际上在Part1里面已经有了很详细的解析。

这部分分析里面提到了handleTopLevelImpl,它在里面进行模拟冒泡。然后针对每个冒泡环节执行handleTopLevel函数。这个函数通过EventPluginHub.extractEvents获取events数组,然后对数组执行批量执行runEventQueueInBatch。

这里需要着重提events数组里面的event合成事件。遍历执行合成事件的本质是遍历event._dispatchListeners进行执行(参考:React Event#extractEvents)。

这里容易疏漏的(可能就我自己容易疏漏😂)是: EventPluginHub.extractEvents获取events数组过程中,会对所有的冒泡元素进行双向的冒泡、捕获模拟(实现的只有冒泡部分),然后将同一种合成事件涉及的所有回调都放在对应合成事件的event._dispatchListeners属性上,最后顺序批量执行。

事件绑定、分发 && 事件体系的融合

第一步

首先是事件绑定的前置条件registrationNameModules。这个Map数据参见之前提到的publishRegistrationName。

在这之前,EventPluginRegistry要先注入eventPluginOrder,并注册好EventPlugins。这之后执行recomputePluginOrdering。这个函数会对plugins数组进行排序。

进一步会将EventPluginRegistry.registrationNameModules && EventPluginRegistry.registrationNameDependencies初始化完毕。

这是两个Map结构的数据,里面键名都是onClick|onChange这些。但是registrationNameModules注册的是EventPlugin,而registrationNameDependencies注册的是EventPlugin对应的dependencies数组。

关于registrationNameDependencies,通俗点讲,针对特定EventPlugin,缓存eventTypes里面的dependencies。以实现指定Plugin,给出registrationName值或者phasedRegistrationNames.bubbled|phasedRegistrationNames.captured值时候,能直接快递查出对应eventName(pluginModule.eventTypes键名)依赖的dependencies数组。

考虑到每个合成事件里面的核心DispatchConfig数据结构,大家可以猜测到它意义是什么——给出一个合成事件,就能查出对应的依赖dependencies数组。

根据这个结论进一步推算,其实registrationNameModules也是走的这个思路。给出一个合成事件,逆向查出其对应EventPlugin。

总结

前置处理,定义好SyntheticEvent核心, 然后在SyntheticEvent上构建EventPlugin。

所有的EventPlugin要注册到EventPluginRegistry。

这个注册目前来说,满足了两个快速:

  1. 要求能通过合成事件或特定属性值能快速逆向查询对应EventPlugin。
  2. 给出合成事件或特定属性值要能快速查出对应dependencies数组。
  3. 根据给定的顺序将EventPlugin放到对应plugins数组里面去。

第二步

然后是事件的分发。这里提到过分发的核心的起始调用ReactEventListener.dispatchEvent.bind(null, topLevelType)

而这里有个topLevelType变量,哪儿来呢?从之前的dependencies数组里面来。我们从这个调用栈分析一下参数的来源(必须自行看下源码才好理解)。

1
2
3
ReactBrowserEventEmitter.listenTo
->ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency)
-->ReactEventListener.dispatchEvent.bind(null, topLevelType)

认真看看,就会发现EventPluginRegistry.registrationNameDependencies[registrationName]被遍历然后作为topLevelType被传入了dispatchEvent。

而这个listenTo的registrationName来自于ReactDomComponent的enqueuePutListener函数。那enqueuePutListener呢?来自_updateDOMProperties执行过程对props值的遍历。

如此 上面这一块就完美闭环了。

不过这之前再细细品味一下这其中体系上的设计。

首先是_updateDOMProperties遍历时要求propsKey in registrationNameModules。其次是后面propsKey后面作为registrationName传递后面,然后又作为topLevelType给了ReactEventListener.dispatchEvent。

这个逻辑开始从dispatchEvent往下走。就会执行到(ReactEventListener#line165):

1
2
3
handleTopLevelImpl({
topLevelType, nativeEvent
})

此时可以进行冒泡模拟了。得模拟冒泡行为把所有需要执行的回调从listenerBank里面捞出来,以备后面顺序批量执行完成最后环节。

回想一下回调在这个地方的保存方式: listenerBank[registrationName][InstanceKey]。此时呢,我们registrationName有了,InstanceKey通过nativeEvent也能拿到。思路是:nativeEvent里面可以获取target,通过ReactDOMComponentTree.getClosestInstanceFromNode可以拿到实例,拿到了实例,也就获取到了InstanceKey。这样我们就可以针对每个冒泡环节拿到对应listener了。

这样整体的逻辑就算通了,React在这里的操作思路是将listener拿到之后,放到对应合成事件的某个属性中,然后批量执行。

具体点说,EventPluginHub,遍历EventPluginRegistry.plugins中的EventPlugins,执行每个EventPlugin上的extractEvents,将返回的合成事件全部返回合并到一个扁平数组中。

这个过程extractEvents返回event过程中,会对event._dispatchListeners & event._dispatchInstances进行赋值,实际就是对上面冒泡模拟提到的listener&实例进行了缓存。

当所有的events数组就位之后(此时listener和实例也就是函数上下文都就位了),然后进入批量执行阶段。走EventPluginHub.processEventQueue。具体实现前面有提到,这里不再赘述。

React事件体系设计

当我们把这些整体都串联完毕,整个React设计体系也就不再神秘。

合成事件部分:

合成事件部分实质上的设计其实很少。在合成事件这里只是设计了统一的接口然后对她进行实现。不通的合成事件的区别大部分都只是对nativeEvent上变量的择选和computed。

合成事件Plugins:

个人认为,这里其实算是整个事件体系的核心中核心。

前面说过,合成事件实质上都紧紧是统一接口的实现,它们本身没有在构造器实现里面核心的dispatchConfig & _targetInst & nativeEvent 变量。这些东西都是在Plugin里面进行的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// jsx
<div onClick={() => console.log(1234)} />
// onClick(内部称为:registrationName)对应的DispatchConfig;
{
phasedRegistrationNames: {
bubbled: 'onClick',
captured: 'onClickCapture',
},
dependencies: ['topClick'], // 'topClick'内部称为topLevelType
}
// step 1:
// 'onClick' 直接直接props获得,可以用来直接
// 从registrationNameDependencies获取dependencies数组
// 也就是['topClick', 'topChange']之类,内部的Map<DispatchConfig>以它做键名
// 换句话说知道了'topClick'才能生成对应的合成事件

// step 2:
// 'topClick'作为快速从Plugin获取这个DispatchConfig的键名使用
// 'topClick'作为快速从Plugin获取需要调用的合成事件类型

// step3:
// 综合step1 + step2,可以知道Plugin可以根据onClick获取特定的合成事件类型(瓶子)
// 也能获取DispatchConfig(酒)

通过Plugin的extractEvents方法,会对模拟冒泡在虚拟DOM进行模拟,把所有回调放到合成事件私有变量中去。

PS: 这种做法会导致如果回调过多,耗时过长,浏览器会卡在微任务无法进行浏览器刷新,也就是常见卡顿说法。

EventPluginRegistry:

遍历所有被inject的Plugin的eventType。除了按顺序保存plugins。还有做两个Map数据结构。

  • Plugin相关的
1
2
3
{
[key: registrationName]: EventPlugin
}

知道registrationName可以获得对应EventPlugin。

  • dependencies相关的
1
2
3
{
[key: registrationName: Array<topLevelType>
}

这里注意phasedRegistrationNames里面的两个值,其实和registrationName一致。

接下来,有registrationName和phasedRegistrationNames其中一个就能获取topLevelType。有了topLevelType,我们就可以获取DispatchConfig,可以获取合成事件类型。

而有了这两样,就可以实例化好一个详细的合成事件。