本文源码基于 React v17.0.2
,React 事件的基础知识就不回顾了,主要从 React 原理切入,从事件注册到事件执行的整个链路。
React 基于浏览器事件机制实现了一套自身的机制,即浏览器原生事件的跨浏览器包装器。
v17 与 v16 相比:
事件注册是在顶层自执行的,在 React 自身引入文件的时候调用的。
注册事件(React 将同种类型的事件放在一个插件中):
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'
// 原生 DOM 事件名称与 React 事件名称映射关系。其中一个作用就是给 allNativeEvents 注入所有原生事件名,下文会用到
SimpleEventPlugin.registerEvents()
EnterLeaveEventPlugin.registerEvents()
ChangeEventPlugin.registerEvents()
SelectEventPlugin.registerEvents()
BeforeInputEventPlugin.registerEvents()
registerEvents
用于初始化原生事件(其中一个作用是为 allNativeEvents
集合注入原生事件名)
在 React 初始化渲染的时候,ReactDOM.render
会调用函数 listenToAllSupportedEvents
来绑定事件
function createRootImpl(container, tag, options) {
// 在根容器上监听支持的事件
const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container
listenToAllSupportedEvents(rootContainerElement)
}
代码位置:packages/react-dom/src/events/DOMPluginEventSystem.js
,只列出 listenToAllSupportedEvents
的核心代码:
function listenToAllSupportedEvents(rootContainerElement) {
if (!rootContainerElement[listeningMarker]) {
// allNativeEvents 是一个 Set 集合,保存所有浏览器原生事件名
allNativeEvents.forEach((domEventName) => {
// 判断是否支持冒泡的事件,不支持的话无需事件委托到根节点
if (!nonDelegatedEvents.has(domEventName)) {
// 冒泡阶段绑定事件
listenToNativeEvent(domEventName, false, rootContainerElement, null)
}
// 捕获阶段绑定事件
listenToNativeEvent(domEventName, true, rootContainerElement, null)
})
}
}
listenToAllSupportedEvents
的核心逻辑:
listenToNativeEvent
来绑定浏览器事件,且绑定在 rootContainerElement
根节点。allNativeEvents
:是一个 Set 集合,保存了 80 个浏览器原生 DOM 事件nonDelegatedEvents
:Set 集合,保存浏览器原生事件中不会冒泡的事件,如 load,scroll,媒体事件 canplay,play 等等接下来看看 listenToNativeEvent
的实现:
function listenToNativeEvent(
domEventName,
isCapturePhaseListener,
rootContainerElement,
targetElement,
eventSystemFlags
) {
// 绑定事件
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener)
}
function addTrappedEventListener(
targetContainer,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
isDeferredListenerForLegacyFBSupport
) {
// 创建事件委托的回调函数(其实是事件派发器)
let listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags)
// 根据事件是捕获阶段还是冒泡阶段,调用不同的绑定函数
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener)
} else {
unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener)
}
}
// 表示在冒泡阶段触发事件处理函数
function addEventBubbleListener(target, eventType, listener) {
// 第三个参数为 false(冒泡阶段)
target.addEventListener(eventType, listener, false) // 需要注意这里的 listener 是事件派发器,并不是我们自己使用时写的事件回调
return listener
}
// 表示在捕获阶段触发事件处理函数
function addEventCaptureListener(target, eventType, listener) {
// 第三个参数为 true(捕获阶段)
target.addEventListener(eventType, listener, true)
return listener
}
再来看看上面的函数 createEventListenerWrapperWithPriority
的实现:
export function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
// 根据事件名获取事件的优先级
const eventPriority = getEventPriorityForPluginSystem(domEventName)
let listenerWrapper
// 根据事件优先级返回对应的事件监听函数
switch (eventPriority) {
// 离散事件
case DiscreteEvent: // 优先级最高
listenerWrapper = dispatchDiscreteEvent
break
// 用户交互阻塞渲染的事件
case UserBlockingEvent: // 优先级适中
listenerWrapper = dispatchUserBlockingUpdate
break
// 其它事件
case ContinuousEvent: // 优先级最低
default:
listenerWrapper = dispatchEvent
break
}
// 返回事件回调函数 listener
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer)
}
由上述代码可以看出,不同的 DOM 事件调用 getEventPriorityForPluginSystem
会返回不同的优先级,优先级包括:
DiscreteEvent
:离散事件。如 click、keydown、focusin 等,这些事件的触发不是连续的,可以快速响应,优先级最高UserBlockingEvent
:用户交互阻塞渲染的事件。如 drag、scroll 等,优先级适中ContinuousEvent
与 default:连续事件和默认事件。连续事件如 playing、load 等,优先级最低而前两个对应的 dispatchDiscreteEvent
和 dispatchUserBlockingUpdate
其实都是对 dispatchEvent
的封装,所以下文我们重点看 dispatchEvent
函数就行了。
listenToNativeEvent
看完上面的代码,让我们来抽离出最核心的代码,把函数调用代码去掉抽离整合如下:
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
const listener = dispatchEvent.bind(null, domEventName, eventSystemFlags, targetContainer)
if (isCapturePhaseListener) {
target.addEventListener(eventType, listener, true)
} else {
target.addEventListener(eventType, listener, false)
}
}
我们也可以知道,事件在根节点中代理后是一直在触发的,只是没有绑定对应的回调函数。
问:React 事件都是在顶层进行代理派发执行的,对不支持冒泡的事件,React 如何触发
在根节点 React 只绑定了不支持冒泡事件的捕获阶段,而实际上 React 会对不支持冒泡的事件(除了 scroll)进行特殊处理,这个过程发生在 DOM 实例的创建阶段(completeWork),React 会直接把事件绑定在具体的元素上
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
switch (tag) {
case 'img':
case 'image':
case 'link':
// We listen to these events in case to ensure emulated bubble
// listeners still fire for error and load events.
listenToNonDelegatedEvent('error', domElement) // 传入事件名与具体的 DOM 元素
listenToNonDelegatedEvent('load', domElement)
props = rawProps
break
// ...
}
}
// 绑定非代理事件
function listenToNonDelegatedEvent(domEventName, targetElement) {
const isCapturePhaseListener = false // 非捕获阶段(冒泡阶段) useCapture: false
const listenerSet = getEventListenerSet(targetElement)
const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener)
if (!listenerSet.has(listenerSetKey)) {
// 绑定事件
addTrappedEventListener(
targetElement, // 绑定到具体 DOM 元素
domEventName,
IS_NON_DELEGATED, // 非代理事件
isCapturePhaseListener
)
listenerSet.add(listenerSetKey)
}
}
dispatchEvent
函数执行时调用的关键函数如下:
1. `attemptToDispatchEvent`
2. `dispatchEventForPluginEventSystem`
3. `dispatchEventsForPlugins`
4. `extractEvents`(`SimpleEventPlugin.extractEvents`...)
5. `processDispatchQueue`
当 DOM 事件触发之后, 会进入到 dispatchEvent
这个回调函数,里面会调用 attemptToDispatchEvent
这个方法,作用是尝试调度事件
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
// ...
// 尝试派发事件
const blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent)
// 尝试派发事件成功,则 return, 下方的代码无需执行
if (blockedOn === null) {
// ...
return
}
// 派发事件
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer)
}
attemptToDispatchEvent
函数的逻辑:
function attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
// 获取触发事件的 DOM 元素(即获取 nativeEvent.target 属性)
const nativeEventTarget = getEventTarget(nativeEvent) // nativeEvent 是原生事件对象
// 根据该 DOM 元素对应的 fiber 节点
let targetInst = getClosestInstanceFromNode(nativeEventTarget)
// ...
// 通过插件系统,派发事件
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer)
}
dispatchEventForPluginEventSystem
会收集 Fiber 节点上的事件,并派发事件(批量更新 batchUpdate)
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
// 打开批处理
batchedEventUpdates(() =>
dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst, targetContainer)
)
}
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
// 获取触发事件的 DOM 元素(即获取 nativeEvent.target 属性)
const nativeEventTarget = getEventTarget(nativeEvent)
// 初始化事件派发队列,用于储存 listener
const dispatchQueue = []
// 1. 创建合成事件,并收集同类型事件添加到 dispatchQueue 中
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
)
// 2. 根据事件派发队列执行事件派发
processDispatchQueue(dispatchQueue, eventSystemFlags)
}
extractEvents
函数会进行事件合成,遍历 Fiber 链表,把收集同类型事件加入到 dispatchQueue
队列。
function extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(...)
ChangeEventPlugin.extractEvents(...)
SelectEventPlugin.extractEvents(...)
BeforeInputEventPlugin.extractEvents(...)
}
}
在合成事件中,会根据 domEventName
来决定使用哪种类型的合成事件。
SimpleEventPlugin
提供了 React 事件系统的基本功能,以 SimpleEventPlugin.extractEvents
为例,看看这个函数内部的关键代码:
SyntheticEventCtor
)dispatchQueue
添加事件(在此注入合成事件实例和收集的同类型事件数组)// 根据原生事件名得到 React 事件名
const reactName = topLevelEventsToReactNames.get(domEventName)
// 合成事件实例
let SyntheticEventCtor = SyntheticEvent
// switch (domEventName) // 不同事件名
// SyntheticEventCtor = xxx // 赋予相应的合成事件构造函数
// 收集节点上所有监听该事件的 listener,向上遍历直到根节点
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly
)
if (listeners.length > 0) {
const event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget)
// 往事件派发队列添加事件(注入合成事件实例与同类型事件监听数组)
dispatchQueue.push({ event, listeners })
}
问:如何收集 DOM 节点上的事件?
答:
<div onClick={() => { console.log('click) }}></div>
React 会给该 div Fiber 节点的 props 上添加 onClick
属性;Fiber 节点中有一个属性 return,通过它可以找到它对应的父节点 Fiber ,这样就可以依次向上遍历父节点的 props 属性有无 onClick
属性,有则添加收集起来,所谓收集也就是从 props 中取出来。
抽离整理 SyntheticEventCtor
的关键实现:
export const SyntheticEvent = createSyntheticEvent(EventInterface)
// 不同事件类型不同的 Interface
function createSyntheticEvent(Interface) {
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
this.isPropagationStopped = functionThatReturnsFalse
// ...
// 在合成事件构造函数的原型上添加
Object.assign(SyntheticBaseEvent.prototype, {
// 阻止默认事件
preventDefault: function () {
if (event.preventDefault) {
event.preventDefault()
}
this.isDefaultPrevented = functionThatReturnsTrue
},
// 阻止冒泡
stopPropagation: function () {
if (event.stopPropagation) {
event.stopPropagation()
}
this.isPropagationStopped = functionThatReturnsTrue
},
// 17 版本去除了事件池,persist 和 isPersistent 都没有用了,但为了向下兼容保留
persist: function () {
// Modern event system doesn't use pooling.
},
isPersistent: functionThatReturnsTrue,
})
}
return SyntheticBaseEvent
}
function functionThatReturnsTrue() {
return true
}
e.preventDefault
和 e.stopPropagation
都是 React 重写封装的,而且是写在合成对象构造函数原型上,且同类型的事件会复用同一个合成事件实例对象。processDispatchQueue
函数:遍历 dispatchQueue
数组 ,调用 processDispatchQueueItemsInOrder
函数派发事件
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
// 是否是捕获阶段,关系到后面执行的顺序
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
:
dispatchListeners
时,是从当前 Fiber 节点遍历至根节点,所以可理解顺序遍历就是冒泡的顺序executeDispatch
真正派发了事件,在 Fiber 节点上绑定的 listener 也就被执行了。function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
let previousInstance
if (inCapturePhase) {
// 捕获阶段,倒序遍历
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i]
// 判断当前是否已停止冒泡了,是则直接 return
// 如果 e.stopPropagation() 方法被调用过,则会一直返回 true,否则默认一直返回 false
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) {
const type = event.type || 'unknown-event'
event.currentTarget = currentTarget
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event)
event.currentTarget = null // 重置
}
由上面可知,React 模拟原生事件捕获与冒泡的执行顺序,本质是靠向上搜集事件后,控制事件的遍历顺序去模拟的。
isPropagationStopped
设置为一个返回 true 的函数,后续派发事件时只要代码判断时则执行函数结果为 true 则表示阻止冒泡,就不再走下面逻辑。接下来读完全文,来总结一下 React 事件机制:
阅读量:183
点赞量:0
收藏量:0