源码结构 关于事件的源码,主要分布在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 ; var E = function ( ) {}; E.prototype = Super.prototype; var prototype = new E(); Object .assign(prototype, Class.prototype); Class.prototype = prototype; Class.prototype.constructor = Class; Class.Interface = Object .assign({}, Super.Interface, Interface); Class.augmentClass = Super.augmentClass; PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler); }; function SyntheticEvent ( dispatchConfig, targetInst, nativeEvent, nativeEventTarget, ) { this .dispatchConfig = dispatchConfig; this ._targetInst = targetInst; this .nativeEvent = nativeEvent; var Interface = this .constructor.Interface; 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]; } } } 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 ; var E = function ( ) {}; E.prototype = Super.prototype; var prototype = new E(); Object .assign(prototype, Class.prototype); Class.prototype = prototype; Class.prototype.constructor = Class; Class.Interface = Object .assign({}, Super.Interface, Interface); Class.augmentClass = Super.augmentClass; PooledClass.addPoolingTo(Class, PooledClass.fourArgumentPooler); }; function SyntheticEvent ( dispatchConfig, targetInst, nativeEvent, nativeEventTarget, ) { this .dispatchConfig = dispatchConfig; this ._targetInst = targetInst; this .nativeEvent = nativeEvent; var Interface = this .constructor.Interface; 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]; } } } 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: 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 , };
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 ; }, };
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。
它核心逻辑如下:
判断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
使用这个函数传入目标参数返回合成事件实例
执行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 = { putListener: function (inst, registrationName, listener ) {}, getListener: function (inst, registrationName ) { }, deleteListener: function (inst, registrationName ) { }, deleteAllListeners: function (inst ) { }, extractEvents: function ( topLevelType, targetInst, nativeEvent, nativeEventTarget, ) { }, enqueueEvents: function (events ) { }, 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; registrationNameModules: {[key: keyof DomAttrEvent]: PluginModule}; registrationNameDependencies: {[key: keyof DomAttrEvent]: Array <keyof topLevelTypes>}, possibleRegistrationNames; injectEventPluginOrder: (injectedEventPluginOrder: EventPluginOrder ) => void ; 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之间的关联、定义、相互作用这些。
这里画个图看看整体关联。
仔细观察这个图里面的脉络,可以看到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 var eventTypes = { mouseEnter: { registrationName: 'onMouseEnter' , dependencies: ['topMouseOut' , 'topMouseOver' ], }, mouseLeave: { registrationName: 'onMouseLeave' , dependencies: ['topMouseOut' , 'topMouseOver' ], }, }; 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 ReactUpdates 、react 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, ) { 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); 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 ) { 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 , );
这个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) { enqueuePutListener(this , propKey, nextProp, transaction); } else if (lastProp) { 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, topEventMapping[dependency], mountAt, ); } 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。
这个注册目前来说,满足了两个快速:
要求能通过合成事件或特定属性值能快速逆向查询对应EventPlugin。
给出合成事件或特定属性值要能快速查出对应dependencies数组。
根据给定的顺序将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 <div onClick={() => console .log(1234 )} /> { phasedRegistrationNames: { bubbled: 'onClick' , captured: 'onClickCapture' , }, dependencies: ['topClick' ], }
通过Plugin的extractEvents方法,会对模拟冒泡在虚拟DOM进行模拟,把所有回调放到合成事件私有变量中去。
PS: 这种做法会导致如果回调过多,耗时过长,浏览器会卡在微任务无法进行浏览器刷新,也就是常见卡顿说法。
EventPluginRegistry:
遍历所有被inject的Plugin的eventType。除了按顺序保存plugins。还有做两个Map数据结构。
1 2 3 { [key: registrationName]: EventPlugin }
知道registrationName可以获得对应EventPlugin。
1 2 3 { [key: registrationName: Array <topLevelType> }
这里注意phasedRegistrationNames里面的两个值,其实和registrationName一致。
接下来,有registrationName和phasedRegistrationNames其中一个就能获取topLevelType。有了topLevelType,我们就可以获取DispatchConfig,可以获取合成事件类型。
而有了这两样,就可以实例化好一个详细的合成事件。