虚拟事件系统实现
开始
在React源码开篇
章节中提到,当使用createRoot
的时候,内部会调用listenToAllSupportedEvents
方法。该方法在React-Dom/src/events/DOMPluginEventSystem.js
文件中定义。所有React
虚拟事件定义的内容都是在events
这个文件夹下的。首先看一下DOMPluginEventSystem.js
文件,当首次引入该文件的时候,会执行以下方法:
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
这些方法均在events/plugins
文件夹下定义。
EventRegistry.js
在讨论上述5个registerEvents
之前,先看一下EventRegistry.js
文件。里面主要定义了两个函数:
registerDirectEvent
export function registerDirectEvent(
// react 里面的事件名称
registrationName: string,
// 真实节点的原生事件名称[数组]
dependencies: Array<DOMEventName>,
) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0;i < dependencies.length;i++) {
allNativeEvents.add(dependencies[i]);
}
}
该函数主要做了两件事:
- 建立
registrationName
和dependencies
的映射关系,并存放到registrationNameDependencies
中。 - 将所有映射过的
dependencies
挨个添加到allNativeEvents
中
registerTwoPhaseEvent
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
该函数调用两次registerDirectEvent
函数,第二次调用为注册的事件增加了Capture
后缀。
registerEvents
再来看下5个registerEvenets
的实现。
SimpleEventPlugin
SimpleEventPlugin.registerEvents
调用的是registerSimpleEvents
方法,在DomEventsProperties.js
文件中定义:
export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = ((simpleEventPluginEvents[i]: any): string);
const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
}
// Special cases where event names don't match.
registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
registerSimpleEvent('dblclick', 'onDoubleClick');
registerSimpleEvent('focusin', 'onFocus');
registerSimpleEvent('focusout', 'onBlur');
registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}
simpleEventPluginEvents
是原生事件组成的一个数组:
const simpleEventPluginEvents = [
'close',
'copy',
'cut',
'drag',
'mouseDown',
// ... 等等一系列的原生事件
];
capitalizedEvent
就是将原生事件名称的第一个字母大写,然后调用registerSimpleEvent
方法:
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
首先会将原生事件名称domEventName
和React
合成事件名称reactName
存放到topLevelEventsToReactNames
中。随后通过registerTwoPhaseEvent
建立两者之间的关系。拿click
来举例,注册后有:
// 原生事件 click
domEventName = 'click'
// 得到 react 的事件名称
reactName = 'onClick'
// 经过registerTwoPhaseEvent绑定关系后
registrationNameDependencies = {
'onClick': ['click'],
'onClickCapture': ['click']
}
allNativeEvents = new Set('click')
EnterLeaveEventPlugin
注册mouseout/mouseover
等事件:
function registerEvents() {
registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}
ChangeEventPlugin
注册onChange
事件,这个事件对应了多个原生事件:
function registerEvents() {
registerTwoPhaseEvent('onChange', [
'change',
'click',
'focusin',
'focusout',
'input',
'keydown',
'keyup',
'selectionchange',
]);
}
SelectEventPlugin
注册onSelect
事件,这个事件对应了多个原生事件:
function registerEvents() {
registerTwoPhaseEvent('onSelect', [
'focusout',
'contextmenu',
'dragend',
'focusin',
'keydown',
'keyup',
'mousedown',
'mouseup',
'selectionchange',
]);
}
BeforeInputEventPlugin
注册了多个合成事件:
function registerEvents() {
registerTwoPhaseEvent('onBeforeInput', [
'compositionend',
'keypress',
'textInput',
'paste',
]);
registerTwoPhaseEvent('onCompositionEnd', [
'compositionend',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionStart', [
'compositionstart',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionUpdate', [
'compositionupdate',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
}
最后
执行完以上5个注册函数后,allNativeEvents
会记录所有已注册的原生事件。registrationNameDependencies
则记录所有原生事件和React
合成事件之间的关系。
listenToAllSupportedEvents
listenToAllSupportedEvents
开始真正监听原生事件。首先会判断有没有注册过,保证原生事件只会注册一次:
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
}
然后开始遍历原生事件,进行监听:
allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
selectionchange
事件不会进行事件冒泡,需要单独在document
上注册。而其他事件则通过listenToNativeEvent
函数注册。
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
let eventSystemFlags = 0;
// 是否是捕捉事件阶段
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
addTrappedEventListener
函数首先会根据事件的名称获取相应的优先级,然后将listener
包装成按优先级执行的listener
。
// 通过事件名称,设置不同的优先级
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
createEventListenerWrapperWithPriority
函数的代码如下:
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 获取事件优先级
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
// 返回包装了优先级的listener
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
getEventPriority
定义如下:
export function getEventPriority(domEventName: DOMEventName): * {
switch (domEventName) {
case 'click':
case 'keydown':
case 'mouseup':
case 'paste':
// ... 等等一系列事件
// 均为同步优先级
return DiscreteEventPriority;
case 'drag':
case 'mousemove':
case 'pointermove':
// ... 等等一系列事件
// 均为连续事件优先级
return ContinuousEventPriority;
case 'message':
// ... 获取 scheduler callback 的优先级
default:
return DefaultEventPriority;
}
}
该函数主要将原生事件分为了DiscreteEventPriority
和ContinuousEventPriority
两大类。
包装后的dispatchDiscreteEvent
和dispatchContinuousEvent
则是在执行代码前会设置全局的优先级,这样在更新的时候通过requestUpdateLane
可以获取到更新对应的优先级。例如:
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
这里的dispatchEvent
就是事件回调执行的内容,后面会提到。
包装完listener
后就需要给DOM
节点添加原生事件了,这个DOM
节点就是container
,即React
渲染内容的容器。
if (isCapturePhaseListener) {
// 捕捉事件阶段
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
// 冒泡阶段阶段
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
分别调用DOM
的原生方法监听原生捕获事件和冒泡事件:
// 冒泡事件
target.addEventListener(eventType, listener, false);
// 捕获事件
target.addEventListener(eventType, listener, true);
dispatchEvent
当真实点击事件触发时,会调用监听函数dispatchEvent
,该函数调用attemptToDispatchEvent
函数:
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.
// 1. 根据事件获取到触发该事件的 目标节点
const nativeEventTarget = getEventTarget(nativeEvent);
// 2. 通过真实节点找到最近的 fiber
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (targetInst !== null) {
// 3. 如果 fiber 存在,看是否已经挂载且在 HostRoot 内部,确保fiber合适。
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
targetInst = null;
}
// ...其他处理
}
// 4. 触发回调
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
return null;
}
首先会根据原生事件触发时的event
获取到相应的触发时的目标元素。随后根据真实节点找到对应的fiber
:
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
let targetInst = (targetNode: any)[internalInstanceKey];
if (targetInst) {
return targetInst;
}
// ...
}
在completeWork
阶段创建真实节点时:
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
// 创建了 element
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
// node . '__reactFiber$' + randomKey = fiber
precacheFiberNode(internalInstanceHandle, domElement);
// node . '__reactProps$' + randomKey = props
updateFiberProps(domElement, props);
return domElement;
}
会执行precacheFiberNode
方法和updateFiberProps
方法,在真实DOM
节点上存放fiber
和props
,因此事件触发时可以获取到对应的fiber
。拿到合适的fiber
后就可以开始触发fiber
上props
里的事件了。
dispatchEventForPluginEventSystem
对于能够进行事件冒泡的事件,通常会执行下列分支代码:
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {}
该分支会向上遍历fiber
,如果遇到了HostRoot
或者HostPortal
,并且container
是一致的:
isMatchingRootContainer(container, targetContainerNode)
那么说明该事件是在container
内部触发的,可以直接触发回调。这里为什么container
为HostPortal
也能监听事件呢?因为在completeWork
阶段有这样一行代码-preparePortalMount
:
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
preparePortalMount(workInProgress.stateNode.containerInfo);
}
bubbleProperties(workInProgress);
return null;
在current
为null
的时候,会在portal
的container
上监听原生事件:
export function preparePortalMount(portalInstance: Instance): void {
listenToAllSupportedEvents(portalInstance);
}
除此之外,如果找不到正确的container
,那么将不会执行事件回调。
监测完container
后,将执行dispatchEventsForPlugins
函数:
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
extractEvents
extractEvents
方法会调用SimpleEvents.extractEvents
。首先获取原生事件对应的reactName
:
// 1. 根据事件名称,获取 react 中对应的事件名称
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
然后根据原生事件名称获取react
中要返回的事件的参数的结构:
// 2. 根据事件名称,获取需要返回的内容的结构
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
switch (domEventName) {
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent;
break;
// ...等等一些列合成事件结构
// ...等等一些列合成事件结构
// ...等等一些列合成事件结构
}
createSyntheticEvent
合成事件返回的结构(即自定义的一个event
对象)通过createSyntheticEvent
定义,它接收一个定义好的参数结构,简化后的代码如下:
function createSyntheticEvent(Interface: EventInterfaceType) {
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber,
nativeEvent: {[propName: string]: mixed},
nativeEventTarget: null | EventTarget,
) {
// ...
return this;
}
Object.assign(SyntheticBaseEvent.prototype, {
// 阻止默认行为
preventDefault: function() {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (event.preventDefault) {
event.preventDefault();
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
// 阻止事件冒泡
stopPropagation: function() {
const event = this.nativeEvent;
if (event.stopPropagation) {
event.stopPropagation();
} else if (typeof event.cancelBubble !== 'unknown') {
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
});
return SyntheticBaseEvent;
}
该方法定义了一个SyntheticBaseEvent
对象,并在该对象上添加了preventDefault
方法和stopPropagation
方法。
function SyntheticBaseEvent(
// react 中监听事件的名称
reactName,
// 原生事件名称
reactEventType,
// 触发事件的节点对应的 fiber
targetInst,
// 原生事件触发时返回的 event 参数
nativeEvent,
// 原生事件节点
nativeEventTarget,
) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
// 如果normalize存在,说明他有自定义的格式化方法
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
// 否则,直接使用原生的值
this[propName] = nativeEvent[propName];
}
}
const defaultPrevented =
nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
该对象会通过不同的Interface
为event
对象设置不同的属性,并且还可以自定义一些”格式化“的函数,方便取值。
紧接着调用accumulateSinglePhaseListeners
计算所有需要触发事件的listener
,大致思路是从下向上遍历找到合适的节点,通过节点创建一个listener
,最后推入到数组当中:
if (tag === HostComponent && stateNode !== null) {
// 1. 当前遍历的真实节点
lastHostComponent = stateNode;
if (reactEventName !== null) {
// 2. 拿到在 react 中定义的事件回调
const listener = getListener(instance, reactEventName);
if (listener != null) {
// 3. 创建一个 listener,并添加到 listeners 中
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
其中getListener
定义如下:
const props = getFiberCurrentPropsFromNode(stateNode);
const listener = props[registrationName];
首先从node
节点上获取对应的props
,然后根据registrationName
获取在react
中注册的事件回调。最后创建一个listener
:
{
instance, // 当前的 fiber
listener, // 当前的事件回调
currentTarget, // 当前的真实节点
}
最后会形成一个从当前节点到根节点之间的一个listeners数组。
如果这个数组长度大于0,那么会根据当前事件类型创建一个event
对象,将事件回调和事件对象加入到dispatchQueue
中:
if (listeners.length > 0) {
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({ event, listeners });
}
processDispatchQueue
最后执行processDispatchQueue
方法,触发事件回调:
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0;i < dispatchQueue.length;i++) {
const { event, listeners } = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
}
processDispatchQueueItemsInOrder
在触发的时候会根据inCapturePhase
判断当前是事件捕捉还是事件冒泡阶段,从而决定listeners
的触发顺序:
for (let i = 0;i < dispatchListeners.length;i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
// 是否阻止事件冒泡
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
executeDispatch
执行react
事件回调,并且执行的时候会将新构建的event
对象作为参数传递。
总结
整个虚拟事件系统的过程大致为:
- 定义
react
合成事件名称,建立起合成事件名称和原生事件名称的关系。 - 监听事件:
- 如果是
HostRoot
,创建时就监听所有原生事件。(包括捕获事件和冒泡事件) - 如果是
HostPortal
,在completeWork
阶段判断current === null
为true
时监听所有事件。 - 如果是不可代理的事件,需要在
completeWork
创建真实节点时,”手动“添加原生事件。
- 触发事件:
- 触发原生事件时,
root
接收到该事件,此时能拿到具体触发事件的真实DOM
元素。 - 通过真实
DOM
获取到相应的fiber
,并进行一系列的校验过程。 - 根据原生事件名称获取对应的
react
事件名称。从当前fiber
向上查找到根节点,将所有监听了该事件的回调保存起来形成listeners
。 - 根据原生的
event
对象创建一个合成的event
对象,可以自定义部分属性。 - 最后根据是捕捉阶段还是冒泡阶段,决定要执行监听事件和
listeners
的执行顺序。
案例
演示代码
export default class extends React.Component {
componentDidMount() {
document.addEventListener('mousedown', () => {
console.log('mouse down event ====>', 'document');
Promise.resolve().then(() => console.log('promise event ====>', 'document'));
});
document.addEventListener('click', () => {
console.log('click event ====>', 'document');
});
}
handleClick(e) {
const id = e.currentTarget.dataset.id;
console.log('click event ====>', id);
}
handleMousedown(e) {
const id = e.currentTarget.dataset.id;
console.log('mouse down event ====>', id);
Promise.resolve().then(() => console.log('promise event ====>', id));
}
render() {
return (
<div onClick={this.handleClick} onMouseDown={this.handleMousedown} data-id="parent">
<div onClick={this.handleClick} onMouseDown={this.handleMousedown} data-id="child">
点击事件和 promise 回调
</div>
</div>
);
}
}
运行结果
// React 16.x 运行结果
mouse down event ====> child
mouse down event ====> parent
promise event ====> child
promise event ====> parent
mouse down event ====> document
promise event ====> document
click event ====> child
click event ====> parent
click event ====> document
结果分析
此处以
React16
为例,React16
是在document
上监听处理合成事件,而React17+
是在root container
上监听处理合成事件,略有不同。
- 当
child
被点击时,原生事件mousedown
冒泡到document
上。 - 首先触发
document
上处理合成事件的回调,此时为一个宏任务。- 合成事件执行:
child
=>parent
,其中产生的微任务,在本次宏任务结束时执行。
- 合成事件执行:
- 其次触发
document
上绑定的其他事件,此时为一个宏任务。- 触发
mouse down event ====> document
,结束后执行微任务。
- 触发
mousedown
执行完成后,开始触发click
事件回调。基本思路同mousedown
。
一些结论
- 合成事件的回调为宏任务,冒泡的过程也是在一个宏任务中进行的。
- 原生事件的回调为宏任务,冒泡的时候,每个节点上监听的原生事件回调都是一个宏任务。
- 如:
child1
=>promise1
=>child2
=>promise2
=>parent1
=>promise3
=> ...
- 如:
- 原生事件如果用
button.click()
这种js
语法调用时,其执行结果区别人addEventListener
监听的结果。- 如:
child1
=>child2
=>promise1
=>promise2
=>parent1
=> ... - 原因是
js
语法调用时,相当于同步触发listeners
。
- 如: