【保姆级】react17 事件机制源码解析
写在前面
react17是一个过渡版本,最大的改动莫过于针对事件机制的重构。虽然现在react18版本已经发布了,但对事件机制这部分几乎是没有改动的,因此这里依然可以对17版本的这部分源码作一次解读。
摘自react官网的独白:
这里把个人觉得较重大的改动框出来了:
- 将事件委托给根节点而不是document;
- 让所有的Capture事件与浏览器捕获阶段保持一致;
- 移除事件池;
恕我直言,你看到的不一定是真实的
-
元素上的事件并不是绑定在本身;
-
event 并不是元素本身的事件对象;
-
整个事件流(捕获、冒泡)过程都是 react 模拟的;
接下来先整个面试题开开胃吧,在 react16.x 版本中,如下代码执行,弹窗组件表现如何?
state={
visible:false
}
componentDidMount(){
document.addEventListener('click',()=>{
this.setState({
visible:false
})
})
}
handleClick = ()=>{
this.setState({visible:true})
}
render(){
return(
<>
<button onClick={this.handleClick}>
点击打开dialog
</button>
{
this.state.visible && <Dialog/>
}
</>
)
}
先卖个关子,看完后面内容就自然知道了😁。
正式开始吧
事件绑定
// packages/react-dom/src/client/ReactDOMRoot.js
function createRootImpl(
container: Container, // 项目根节点
tag: RootTag,
options: void | RootOptions,
) {
if (enableEagerRootListeners) {
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
// 注意此函数 监听所有支持的事件
listenToAllSupportedEvents(rootContainerElement);
}
return root;
}
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (enableEagerRootListeners) {
// allNativeEvents 这个变量是所有原生事件的集合
allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false,
((rootContainerElement: any): Element),
null,
);
}
listenToNativeEvent(
domEventName,
true,
((rootContainerElement: any): Element),
null,
);
});
}
}
这里有一个疑问 allNativeEvents 这个变量是怎么赋值的?实际上 packages/react-dom/src/events/DOMPluginEventSystem.js 在这个文件的顶部
// packages/react-dom/src/events/DOMPluginEventSystem.js
import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
在引入插件的同时调用了插件对应的事件注册方法,关于插件的内容在后续讲解针对事件源event的处理时再来讨论。
// packages/react-dom/src/events/EventRegistry.js
export const allNativeEvents: Set<DOMEventName> = new Set();
export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
在搞清楚 allNativeEvents 的来源后我们继续往下
// packages/react-dom/src/events/DOMPluginEventSystem.js
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 特别注意这个地方 对事件的回调函数作了一次包装 稍后在事件触发阶段再来详细聊聊
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let unsubscribeListener;
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
最后我们来到了真正的事件绑定的地方
/*
packages/react-dom/src/events/EventListener.js
*/
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
最后我们在根节点上绑定事件大概就是这样的:
以上就是事件绑定全部流程,我们来小结一下:
主要是对事件的捕获和冒泡阶段进行分别绑定,eventType 指的是事件类型,target 就是指的是我们应用程序的根节点,这里与16版本的区别主要是:
- 17版本是将事件委托到了根节点上,这样有利于微前端的应用。
- 17版本是将所有合法事件在应用程序初始化的时候都绑定上了,而16版本是按需绑定,因此在16版本react事件与原生事件有一个映射关系,比如 onChange:[blur,input,focus...]。
事件触发
在进行下面内容之前,我们最好得先有一个思维模型:
- 在上个阶段根节点绑定的原生事件中回调函数,其实是经过react底层包装过的,DOM事件流:捕获阶段 ===> 目标阶段 ===> 冒泡阶段,react其实是在底层模拟了这些过程。
- 事件执行是收集react元素上的事件监听;因此如果原生事件如果阻止了事件冒泡,那么后续的收集过程也就不执行了,自然react事件也就不执行了。
现在我们回到上面提到过的
packages/react-dom/src/events/DOMPluginEventSystem.js 文件,
来看看createEventListenerWrapperWithPriority这个方法:
// packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
先忽略事件优先级的判断,因此我们将关注点放在 dispatchEvent 这个方法,可以得知该方法是所有监听事件的回调函数,通过bind传入了额外的参数,我们来看看这个方法的具体实现:
// packages/react-dom/src/events/ReactDOMEventListener.js
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}
// 此处是根据dom查找对应fiber的关键
const blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn === null) {
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
首先来看看接收的参数 domEventName、eventSystemFlags、targetContainer 这三个参数是通过上面的 bind 方法传入的,nativeEvent 则是原生的事件对象。在这个方法中执行 attemptToDispatchEvent 这里会尝试添加派发事件
// packages/react-dom/src/events/ReactDOMEventListener.js
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// We're not blocked on anything.
return null;
}
注意这个函数 getClosestInstanceFromNode:从真实dom中找到对应的fiber,具体是怎么做的呢?
// packages/react-dom/src/client/ReactDOMHostConfig.js
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
// packages/react-dom/src/client/ReactDOMComponentTree.js
const randomKey = Math.random()
.toString(36)
.slice(2);
const internalInstanceKey = '__reactFiber$' + randomKey;
export function precacheFiberNode(
hostInst: Fiber,
node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
): void {
(node: any)[internalInstanceKey] = hostInst;
}
其实就是在处理fiber的complete阶段中,在真实dom节点上添加了一个 internalInstanceKey 属性指向对应的fiber节点。记住这里,下文要用到。
回到事件派发,接着往下:
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
batchedEventUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
注意这里有批量更新的处理就是这里 batchedEventUpdates ,比如我们在点击事件中多次setState,在这里都是都会做一次批量处理合成一次,写到这我突然又想起了一道面试题:如何打破react的批量更新?答案是:将执行代码放在setTimeout中,但仅适用于类组件中。
接着往下:
// packages/react-dom/src/events/DOMPluginEventSystem.js
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
在这里我们需要关注两个地方:dispatchQueue、extractEvents,dispatchQueue这个变量储存的是需要触发的事件队列。同时我们也终于看到了react合成事件的核心了:extractEvents,还记得文章的开头讲的event事件对象其实并不是原生的,而是经过react底层包装过的。
// packages/react-dom/src/events/DOMPluginEventSystem.js
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
if (shouldProcessPolyfillPlugins) {
// 省略部分代码...
}
}
在这个函数中解释了,react为什么要引入事件插件的原因?因为在不同类型的事件中,比如onClick,onChange的事件对象中需要进行不同的处理(polyfill)。
// packages/react-dom/src/events/plugins/SimpleEventPlugin.js
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
if (
enableCreateEventHandleAPI &&
eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
) {
// 略...
} else {
const accumulateTargetOnly =
!inCapturePhase &&
// TODO: ideally, we'd eventually add all events from
// nonDelegatedEvents list in DOMPluginEventSystem.
// Then we can remove this special list.
// This is a breaking change that can wait until React 18.
domEventName === 'scroll';
// 根据对应fiber向上递归找到整个react事件队列
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
);
if (listeners.length > 0) {
// Intentionally create event lazily.
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
}
}
这里我们需要关注这几个地方:
- accumulateSinglePhaseListeners 函数执行结果得到元素上的所有react事件,比如onClick、onChange。
- 合成event事件对象构造函数 SyntheticEventCtor;
先来看accumulateSinglePhaseListeners函数:
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
): Array<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;
let lastHostComponent = null;
while (instance !== null) {
const {stateNode, tag} = instance;
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
// Standard React on* listeners, i.e. onClick or onClickCapture
if (reactEventName !== null) {
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
if (accumulateTargetOnly) {
break;
}
instance = instance.return;
}
return listeners;
}
在真正理解这个函数之前,我们得回顾一下上面提到过的 根据dom获取对应的fiber节点(getClosestInstanceFromNode函数),这里其实就是将递归的从子fiber到祖先fiber上的props属性上依次收集react事件。
接下来我们直接看是如何生成事件对象的:
// packages/react-dom/src/events/SyntheticEvent.js
function createSyntheticEvent(Interface: EventInterfaceType) {
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber,
nativeEvent: {[propName: string]: mixed},
nativeEventTarget: null | EventTarget,
) {
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;
}
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;
}
Object.assign(SyntheticBaseEvent.prototype, {
preventDefault: function() {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.preventDefault) {
event.preventDefault();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation();
// $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
// 兼容ie
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
persist: function() {
// Modern event system doesn't use pooling.
},
isPersistent: functionThatReturnsTrue,
});
return SyntheticBaseEvent;
}
这是一个工厂函数,首先定义了一个基础的事件对象类:SyntheticBaseEvent,在此原型链上,添加了preventDefault、stopPropagation方法来对应原生事件。仔细看这两个方法的实现过程不难看出里面对ie浏览器做了兼容处理,此处应该给react鼓掌👏🏻。同时也解释了react费劲巴拉的搞出一个事件机制的原因:试图抹平程序运行在各浏览器的差异。另外此函数接受了一个名为Interface的参数,目的是用作在实例化 SyntheticBaseEvent 类时,将不同的事件类型需要的属性挂到this上去。比如:
const EventInterface = {
eventPhase: 0,
bubbles: 0,
cancelable: 0,
timeStamp: function(event) {
return event.timeStamp || Date.now();
},
defaultPrevented: 0,
isTrusted: 0,
};
export const SyntheticEvent = createSyntheticEvent(EventInterface);
const UIEventInterface: EventInterfaceType = {
...EventInterface,
view: 0,
detail: 0,
};
export const SyntheticUIEvent = createSyntheticEvent(UIEventInterface);
聊完了合成事件对象,接下来就是执行我们的事件监听了,在这里最主要的就是模拟事件捕获和冒泡:
// packages/react-dom/src/events/DOMPluginEventSystem.js
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 执行用户定义的react函数
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const {instance, currentTarget, listener} = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 执行用户定义的react函数
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
上文我们讲过事件是通过自下而上的方式收集的,这里很明显的可以看出通过反向遍历事件队列来模拟了事件的捕获,然后再次通过正向遍历的方式模拟事件的冒泡。同时在两次遍历的过程中,如果在某次事件执行了 e.stopPropagation(),实际上调用的是合成event对象中自定义的 stopPropagation 方式,同时isPropagationStopped()返回结果为true,自然在此处的遍历就阻止了事件的继续传播。自此,我们整个事件机制就已经串起来了。
现在我们也可以回答开篇提到的那道面试了:
首先弹框永远不会显示,原因是react16版本事件是委托到document上的,当点击按钮后visible置为true,但事件冒泡到document后又将visible置为了false。讲到这有些同学可能会说,那还不简单,在按钮点击事件上加一个e.stopPropagation()不就好了?答案是否定的,e.stopPropagation()只能阻止事件向上冒泡,而在这里按钮事件是被委托到document上的,它们是同级而不是上下级。只能这么解决:e.nativeEvent.stopImmediatePropagation(),这个方法可以阻止事件同级传播。
但在17版本就不会存在这个问题,直接用e.stopPropagation()就可以解决问题,因为17版本事件是被委托到了根节点而不是document。
总结
简单来说整个react17的事件机制,可分为两个流程:
- 事件绑定
在这个过程中,首先在react程序初始化的时候就往根节点上注册了所有合法的原生事件,并通过 dispatchEvent 函数作为事件的统一回调函数;
- 事件触发
上面定义的 dispatchEvent 事件回调函数被触发,分别执行了 从真实dom上查找对应的fiber、事件批量更新的处理、根据不同的事件类型合成不同的事件对象(event)包括重新定义了 stopPropagation preventDefault 并做了ie兼容处理、在对应的fiber节点自下而上递归遍历出react事件、最后根据正向、反向遍历react事件队列分别模拟出事件的捕获与冒泡阶段。
以上就是文章的全部内容,欢迎各位大佬的指正。码字不易,还请看官动动小手点赞评论😁
转载自:https://juejin.cn/post/7136506198832087053