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