React18 事件系统源码解析
前置知识
事件流包含三个阶段, 按照以下顺序依次执行
1. 事件捕获阶段
2. 处于目标阶段
3. 事件冒泡阶段
原生事件会产生一些跨平台的兼容问题
- 阻止冒泡
- 在微软的模型中你必须设置事件的
cancelBubble的属性为 true - 在 W3C 模型中你必须调用事件的
stopPropagation()方法
function stopPropagation(event) {
if (!event) {
window.event.cancelBubble = true;
}
if (event.stopPropagation) {
event.stopPropagation();
}
}
- 阻止默认事件
- 在微软的模型中你必须设置事件的
returnValue的属性为 false - 在 W3C 模型中你必须调用事件的
preventDefault()方法
function preventDefault(event) {
if (!event) {
window.event.returnValue = false
}
if (event.preventDefault) {
event.preventDefault()
}
}
基于以上等一些跨平台的浏览器兼容问题,React 内部实现了一套自己的合成事件。
React 事件行为
import { useRef, useEffect } from 'react'
import { createRoot } from 'react-dom/client'
function App() {
const parentRef = useRef()
const childRef = useRef()
const parentBubble = () => {
console.log('父元素React事件冒泡')
}
const childBubble = () => {
console.log('子元素React事件冒泡')
}
const parentCapture = () => {
console.log('父元素React事件捕获')
}
const childCapture = () => {
console.log('子元素React事件捕获')
}
useEffect(() => {
parentRef.current.addEventListener(
'click',
() => {
console.log('父元素原生捕获')
},
true
)
parentRef.current.addEventListener('click', () => {
console.log('父元素原生冒泡')
})
childRef.current.addEventListener(
'click',
() => {
console.log('子元素原生捕获')
},
true
)
childRef.current.addEventListener('click', () => {
console.log('子元素原生冒泡')
})
document.addEventListener(
'click',
() => {
console.log('document原生捕获')
},
true
)
document.addEventListener('click', () => {
console.log('document原生冒泡')
})
}, [])
return (
<div ref={parentRef} onClick={parentBubble} onClickCapture={parentCapture}>
<p ref={childRef} onClick={childBubble} onClickCapture={childCapture}>
事件执行顺序
</p>
</div>
)
}
const root = createRoot(document.getElementById('root'))
root.render(<App />)
大家不看答案的情况下,如果可以说出以上代码打印的日志,那说明对 React 事件掌握的不错。
好,接下来揭晓一下答案

解析触发过程
上面代码层级如下 document -> root -> div -> p
React17-18 中是采用了事件代理的形式,将事件挂载到了 #root 上,所以。
document注册的事件最先触发。root注册的事件触发,React 根据当前点击的event.target收集对应 DOM 节点的fiber节点中的pendingProps,pendingProps在这里简单理解就是jsx中 DOM 节点对应的props,收集props中的onClickCapture(因为触发的是点击事件,所以收集onClickCapture捕获函数),最终在队列中收集成[childCapture, parentCapture],然后倒序触发。div注册的捕获事件触发。p注册的捕获事件触发。p注册的冒泡事件触发。div注册的冒泡事件触发。root收集的队列里有两个冒泡事件,[childBubble, parentBubble], 然后正序触发。document注册的冒泡事件触发。
源码部分
入口文件 ReactDOMRoot 文件中的 listenToAllSupportedEvents 方法,在 root 根 fiber 创建完之后绑定事件
1. 绑定所有简单事件
SimpleEventPlugin.registerEvents();
const listeningMarker =
'_reactListening' +
Math.random()
.toString(36)
.slice(2);
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
}
}
SimpleEventPlugin.registerEvents()方法会为我们的allNativeEvents注册事件,allNativeEvents是一个Set集合。- 首先看
#root节点上是否绑定过事件代理了,如果第一次绑定就rootContainerElement[listeningMarker] = true,防止多次代理。 allNativeEvents是所有的原生事件(内容比较多,可点击跳转源码搜索 simpleEventPluginEvents),因为有一些事件是没有冒泡行为的,比如scroll事件等 ,所以在这里根据nonDelegatedEvents区分一下是否需要绑定冒泡事件。listenToNativeEvent其实就是给我们的root事件通过addEventListener来绑定真正的事件,实现事件代理。
2. SimpleEventPlugin.registerEvents
说一下 allNativeEvents 赋值的过程
- 调用SimpleEventPlugin插件的registerEvents方法注册事件
SimpleEventPlugin.registerEvents();
- registerSimpleEvents
// registerEvents 就是 registerSimpleEvents,内部导出的时候重命名了
let topLevelEventsToReactNames = new Map()
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);
}
}
function registerSimpleEvent(domEventName: DOMEventName, reactName: string) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
export function registerDirectEvent(registrationName, dependencies) {
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i])
}
}
eventName就是click、drag、close等这些简单事件。capitalizedEvent就是React里绑定的事件了,比如上述的click、drag、close会转成onClick,onDrag,onClose。topLevelEventsToReactNames是个Map,用来建立原生事件跟React事件的映射,到时候根据触发的事件来找到 React 里映射的事件,收集fiber上的props对应的事件。registerTwoPhaseEvent方法注册捕获 + 冒泡阶段的事件。registerDirectEvent是真正的给allNativeEvents这个Set赋值。
OK,到这里我们 root 节点就已经通过事件管理绑定了所有的简单事件。
接下来就是事件触发的过程
3. 事件触发的函数
- dispatchDiscreteEvent
/**
* @param {*} domEventName click 等事件名
* @param {*} eventSystemFlags 0 冒泡 4 捕获
* @param {*} container #root 根节点
* @param {*} nativeEvent 原生事件 event
*/
function dispatchDiscreteEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
container: EventTarget,
nativeEvent: AnyNativeEvent,
) {
try {
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}
}
- dispatchEvent
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
- dispatchEventOriginal
function dispatchEventOriginal(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
- dispatchEventForPluginEventSystem
export function dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
)
}
- dispatchEventsForPlugins
/**
*
* @param {*} domEventName click 等原生事件
* @param {*} eventSystemFlags 0是冒泡,4 是捕获
* @param {*} nativeEvent 原生事件 event
* @param {*} targetInst 当前点击 DOM 节点对应的 fiber 节点
* @param {*} targetContainer #root
*/
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);
}
export function processDispatchQueue(dispatchQueue, eventSystemFlags) {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0 //true 就是捕获
for (let i = 0; i < dispatchQueue.length; i++) {
const { event, listeners } = dispatchQueue[i]
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase)
}
}
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;
}
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;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
function executeDispatch(event, listener, currentTarget) {
event.currentTarget = currentTarget
listener(event)
event.currentTarget = null
}
getEventTarget就是获取event.target,也就是我们在页面上点击的 DOM 节点。extractEvents方法会根据传过去的domEventName(比如这个事件是click),去targetInst这个 fiber 节点上去收集props里的onClick事件,fiber.return指的就是targetInst的父fiber,比如当前点击的p标签,targetInst就是p标签的 fiber,targetInst.return指的就是div的fiber节点,然后一直递归收集到dispatchQueue队列里面,最终dispatchQueue队列的数据结构就是[{event:合成事件源, listener: [{instance: p 标签的 fiber,listener:对应 p 标签的 onClick 事件,currentTarget:p 标签 DOM 节点}, div 标签的 {instance, listener, currentTarget}]}]。processDispatchQueue开始去执行我们收集的事件。processDispatchQueueItemsInOrder会判断如果当前是捕获阶段,那就倒序遍历执行我们的dispatchListeners,如果是冒泡阶段,就正序遍历执行dispatchListeners,遍历过程中还需要更改事件源上的currentTarget属性。- 整个事件阶段就完成了。
转载自:https://juejin.cn/post/7191308289177550906