事件系统
事件系统
原生 DOM 事件
注册事件
设置事件目标的事件属性。事件处理属性由
on
+ 事件名组成(比如:onchange
、onload
、onmouseover
等)。window.onload = function () { const shippingAddressDOM = document.getElementById('shippingAddress') shippingAddressDOM.onsubmit = function () { return this } }
设置 HTML 标签元素的事件属性。
<button onclick="alert('Hello World!')"></button>
当指定一串 JavaScript 代码作为 HTML 事件处理程序属性的值时,浏览器会把代码转换为类似如下的函数
function (event) { with(document) { with(this.form || {}) { with(this) { /* ... 这里是编码 */ } } } }
EventTarget.addEventListener()
:将指定的监听器注册到EventTarget
上,当该对象触发指定的事件时,指定的回调函数就会被执行。事件目标可以是一个文档上的元素Element
、Document
和Window
,以及任何支持事件的对象(比如XMLHttpRequest
)。addEventListener(type, listener, useCapture)
type
: 监听的事件类型(名称),大小写敏感。listener
: 监听函数。当所监听的事件类型触发时,会调用该监听函数。useCapture
: 可选,布尔值。true
表示监听函数将在捕获阶段(capture)触发,默认值为false
(监听函数只在冒泡阶段被触发)。
事件流
事件流分为 3 个阶段:
- 捕获阶段(Capturing phase) :从
window
对象传导到目标节点。 - 目标阶段(Target phase) :在目标节点上触发。
- 冒泡阶段(Bubbling phase) :从目标节点传导回
window
对象(从底层传回上层)。并不是所有的事件都会冒泡,有些事件并不存在冒泡事件,如:blur
、focus
、mouseenter
等
阻止事件冒泡:
event.stopPropagation()
:如果当前元素上存在其他处理程序都会继续运行。同时,不能防止任何默认行为的发生,可以使用event.preventDefault()
阻止事件触发后默认动作的发生。event.stopImmediatePropagation()
: 如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行stopImmediatePropagation()
,那么剩下的事件监听器都不会被调用。
event.eventPhase
获取事件流当前处于的阶段,返回一个代表当前执行阶段的整数值。
0
: 没有事件正在被处理1
: 表示处于捕获阶段(Capturing phase)2
: 表示处于目标阶段(Target phase)3
: 表示处于冒泡阶段(Bubbling phase)
事件委托(代理)
由于事件会在冒泡阶段向上传播到父节点,可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。
var ul = document.querySelector('ul')
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// do something ...
}
})
React 合成事件(SyntheticEvent)
合成事件(SyntheticEvent):React 跨浏览器原生事件包装器。具有与浏览器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
,除了事件在所有浏览器中他们工作方式都相同。
采用合成事件模式的好处:
- 兼容性,跨平台。每个浏览器的内核都不相同,React 通过顶层事件代理机制,保证冒泡的统一性,抹平不同浏览器事件对象之间的差异,将不同平台的事件进行模拟成合成事件,使其能够跨浏览器执行。
- 将所有事件统一管理。在原生事件中,所有的事件都绑定在对应的 DOM 上,如果页面复杂,绑定的事件会非常多,这样就会造成一些不可控的问题。
- 避免垃圾回收。事件会被频繁的创建和回收,会影响性能。为了解决这个问题,React 引入事件池,通过事件池来获取和释放事件。所有的事件并不会被释放,而是存入到一个数组中,如果这个事件触发,则直接在这个数组中弹出即可,避免了频繁创建和销毁。
与原生事件的区别
React 事件使用驼峰命名,而不是全部小写(比如:
onClick
);原生事件使用纯小写命名(比如:onclick
)。通过 JSX , 传递一个函数作为事件处理程序;原生事件传递的是字符串。
事件源不同,阻止默认事件的方式不同。
在 React 中,获取到的事件源(
event
)并非是真正的事件event
,而是经过 React 单独处理的event
。- 在合成事件中,事件处理程序返回 false 将不再停止事件冒泡。 应该根据需要手动
e.stopPropagation()
或e.preventDefault()
。 - 在原生事件中,可以通过
e.preventDefault()
和return false
来阻止默认事件。
- 在合成事件中,事件处理程序返回 false 将不再停止事件冒泡。 应该根据需要手动
处理捕获阶段的注册事件。
在 React 中,所有的绑定事件(如:
onClick
、onChange
)都是冒泡阶段执行。为捕获阶段注册事件处理程序,需要将Capture
附加到事件名称中(如:onClickCapture
)。
合成事件与原生事件的执行顺序
- React 16.x :
document
捕获 --> 原生事件捕获 --> 原生事件冒泡 --> React 合成事件捕获 --> React 合成事件冒泡 -->document
冒泡 - React 17.x / 18.x :
document
捕获 --> React 合成事件捕获 --> 原生事件捕获 --> 原生事件冒泡 --> React 合成事件冒泡 -->document
冒泡
function EventCompound() {
useEffect(() => {
const div = document.getElementById('div')
const button = document.getElementById('button')
div.addEventListener('click', () => console.log('原生冒泡:div元素'))
button.addEventListener('click', () => console.log('原生冒泡:button元素'))
div.addEventListener('click', () => console.log('原生捕获:div元素'), true)
button.addEventListener(
'click',
() => console.log('原生捕获:button元素'),
true
)
document.addEventListener('click', () => console.log('document元素冒泡'))
document.addEventListener(
'click',
() => console.log('document元素捕获'),
true
)
}, [])
return (
<div
id="div"
onClick={() => console.log('React冒泡:div元素')}
onClickCapture={() => console.log('React捕获:div元素')}
>
<button
id="button"
onClick={() => console.log('React冒泡:button元素')}
onClickCapture={() => console.log('React捕获:button元素')}
>
执行顺序 v16/v17/v18
</button>
</div>
)
}
React 16 事件系统
事件池
注意
事件池仅适用于 React 16 及更早版本,Web 端的 React 17+ 不再使用事件池。
事件池简介
React 合成事件(SyntheticEvent)对象会被放入事件池中统一管理,不同类型的合成事件对应不同的事件池。
合成事件(SyntheticEvent)对象可以被复用,当所有事件处理函数被调用之后,其所有属性都会被置空。如果需要在事件处理函数运行之后获取事件对象的属性,需要调用 e.persist()
function ExampleEventCompound() {
const handleChange = event => {
event.persist()
console.log('input value', event.target.value)
setTimeout(() => {
/* 如果未调用 e.persist() ,会提示警告,event 属性被设置为空。 */
console.log('setTimeout input value', event.target.value)
}, 100)
}
return <input onChange={handleChange} />
}
事件池分析
在点击事件中,实际上会调用 SimpleEventPlugin
中 extractEvents
方法,返回 event
。其中,event
通过调用 EventConstructor.getPooled
方法生成。
SimpleEventPlugin
// packages\react-dom\src\events\SimpleEventPlugin.js
const SimpleEventPlugin: PluginModule<MouseEvent> = {
// simpleEventPluginEventTypes gets populated from
// the DOMEventProperties module.
eventTypes: simpleEventPluginEventTypes,
extractEvents: function (
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: MouseEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags
): null | ReactSyntheticEvent {
const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType)
if (!dispatchConfig) {
return null
}
let EventConstructor
switch (topLevelType) {
case DOMTopLevelEventTypes.TOP_KEY_PRESS:
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(nativeEvent) === 0) {
return null
}
/* falls through */
case DOMTopLevelEventTypes.TOP_KEY_DOWN:
case DOMTopLevelEventTypes.TOP_KEY_UP:
EventConstructor = SyntheticKeyboardEvent
break
case DOMTopLevelEventTypes.TOP_BLUR:
case DOMTopLevelEventTypes.TOP_FOCUS:
EventConstructor = SyntheticFocusEvent
break
case DOMTopLevelEventTypes.TOP_CLICK:
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return null
}
/* falls through */
case DOMTopLevelEventTypes.TOP_AUX_CLICK:
case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK:
case DOMTopLevelEventTypes.TOP_MOUSE_DOWN:
case DOMTopLevelEventTypes.TOP_MOUSE_MOVE:
case DOMTopLevelEventTypes.TOP_MOUSE_UP:
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case DOMTopLevelEventTypes.TOP_MOUSE_OUT:
case DOMTopLevelEventTypes.TOP_MOUSE_OVER:
case DOMTopLevelEventTypes.TOP_CONTEXT_MENU:
EventConstructor = SyntheticMouseEvent
break
case DOMTopLevelEventTypes.TOP_DRAG:
case DOMTopLevelEventTypes.TOP_DRAG_END:
case DOMTopLevelEventTypes.TOP_DRAG_ENTER:
case DOMTopLevelEventTypes.TOP_DRAG_EXIT:
case DOMTopLevelEventTypes.TOP_DRAG_LEAVE:
case DOMTopLevelEventTypes.TOP_DRAG_OVER:
case DOMTopLevelEventTypes.TOP_DRAG_START:
case DOMTopLevelEventTypes.TOP_DROP:
EventConstructor = SyntheticDragEvent
break
case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL:
case DOMTopLevelEventTypes.TOP_TOUCH_END:
case DOMTopLevelEventTypes.TOP_TOUCH_MOVE:
case DOMTopLevelEventTypes.TOP_TOUCH_START:
EventConstructor = SyntheticTouchEvent
break
case DOMTopLevelEventTypes.TOP_ANIMATION_END:
case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION:
case DOMTopLevelEventTypes.TOP_ANIMATION_START:
EventConstructor = SyntheticAnimationEvent
break
case DOMTopLevelEventTypes.TOP_TRANSITION_END:
EventConstructor = SyntheticTransitionEvent
break
case DOMTopLevelEventTypes.TOP_SCROLL:
EventConstructor = SyntheticUIEvent
break
case DOMTopLevelEventTypes.TOP_WHEEL:
EventConstructor = SyntheticWheelEvent
break
case DOMTopLevelEventTypes.TOP_COPY:
case DOMTopLevelEventTypes.TOP_CUT:
case DOMTopLevelEventTypes.TOP_PASTE:
EventConstructor = SyntheticClipboardEvent
break
case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE:
case DOMTopLevelEventTypes.TOP_POINTER_CANCEL:
case DOMTopLevelEventTypes.TOP_POINTER_DOWN:
case DOMTopLevelEventTypes.TOP_POINTER_MOVE:
case DOMTopLevelEventTypes.TOP_POINTER_OUT:
case DOMTopLevelEventTypes.TOP_POINTER_OVER:
case DOMTopLevelEventTypes.TOP_POINTER_UP:
EventConstructor = SyntheticPointerEvent
break
default:
// HTML Events
// @see http://www.w3.org/TR/html5/index.html#events-0
EventConstructor = SyntheticEvent
break
}
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget
)
accumulateTwoPhaseDispatches(event)
return event
},
}
export default SimpleEventPlugin
EventConstructor.getPooled
实际调用为 SyntheticEvent
中的 getPooledEvent
方法,当 EventConstructor.eventPool
存在时会复用事件对象,否则会创建新的对象。
在 getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst)
方法中:
dispatchConfig
:该参数将事件对应的 React 元素实例、原生事件、原生事件对应的 DOM 封装成了一个合成事件。比如,冒泡事件中的onClick
和捕获事件中的onClickCapture
targetInst
:组件的实例,通过e.target
(事件源) 得到对应的ReactDomComponent
nativeEvent
:对应原生事件对象nativeInst
:原生的事件源
// packages\legacy-events\SyntheticEvent.js
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
const EventConstructor = this
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop()
EventConstructor.call(
instance,
dispatchConfig,
targetInst,
nativeEvent,
nativeInst
)
return instance
}
return new EventConstructor(
dispatchConfig,
targetInst,
nativeEvent,
nativeInst
)
}
事件池填充合成事件对象时,会调用 SyntheticEvent
中的 releasePooledEvent
方法。会执行 event.destructor()
方法重置 event
的部分属性,如果事件池没有满,则会填充进去。
// packages\legacy-events\SyntheticEvent.js
const EVENT_POOL_SIZE = 10
function releasePooledEvent(event) {
const EventConstructor = this
invariant(
event instanceof EventConstructor,
'Trying to release an event instance into a pool of a different type.'
)
event.destructor()
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event)
}
}
事件插件初始化
React 将合成事件与原生事件的对应关系存放在 React 事件插件(EventPlugin)中。事件插件可以认为是 React 将不同的合成事件处理函数封装成了一个模块,每个模块处理对应的合成事件。例如:
onClick
事件通过SimpleEventPlugin
插件进行处理onChange
事件通过ChangeEventPlugin
插件进行处理onMouseEnter
、onMouseLeave
通过EnterLeaveEventPlugin
插件进行处理- ......
注册事件插件
为处理合成事件与原生事件的对应关系,React 采用了初始化注册事件插件的方式。通过执行 EventPluginRegistry
中的 injectEventPluginsByName()
方法进行相关事件插件注册。
// packages\react-dom\src\client\ReactDOMClientInjection.js
/**
* Some important event plugins included by default (without having to require
* them).
*/
injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
})
执行
EventPluginRegistry
中的injectEventPluginsByName()
函数生成namesToPlugins
,然后执行recomputePluginOrdering
。生成的namesToPlugins
如下:const namesToPlugins = { SimpleEventPlugin, EnterLeaveEventPlugin, ChangeEventPlugin, SelectEventPlugin, BeforeInputEventPlugin, }
injectEventPluginsByName(injectedNamesToPlugins) 函数
// packages\legacy-events\EventPluginRegistry.js export function injectEventPluginsByName( injectedNamesToPlugins: NamesToPlugins ): void { let isOrderingDirty = false for (const pluginName in injectedNamesToPlugins) { if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) { continue } const pluginModule = injectedNamesToPlugins[pluginName] if ( !namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== pluginModule ) { invariant( !namesToPlugins[pluginName], 'EventPluginRegistry: Cannot inject two different event plugins ' + 'using the same name, `%s`.', pluginName ) namesToPlugins[pluginName] = pluginModule isOrderingDirty = true } } if (isOrderingDirty) { recomputePluginOrdering() } }
执行
recomputePluginOrdering()
函数生成plugins
数组(存储注册的所有插件列表,初始化为空),然后执行publishEventForPlugin
。生成的plugins
如下:const plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
recomputePluginOrdering 函数
// packages\legacy-events\EventPluginRegistry.js function recomputePluginOrdering(): void { if (!eventPluginOrder) { // Wait until an `eventPluginOrder` is injected. return } for (const pluginName in namesToPlugins) { const pluginModule = namesToPlugins[pluginName] const pluginIndex = eventPluginOrder.indexOf(pluginName) invariant( pluginIndex > -1, 'EventPluginRegistry: Cannot inject event plugins that do not exist in ' + 'the plugin ordering, `%s`.', pluginName ) if (plugins[pluginIndex]) { continue } invariant( pluginModule.extractEvents, 'EventPluginRegistry: Event plugins must implement an `extractEvents` ' + 'method, but `%s` does not.', pluginName ) plugins[pluginIndex] = pluginModule const publishedEvents = pluginModule.eventTypes for (const eventName in publishedEvents) { invariant( publishEventForPlugin( publishedEvents[eventName], pluginModule, eventName ), 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName ) } } }
执行
publishEventForPlugin()
函数生成registrationNameModules
对象(React 合成事件与对应事件插件的映射关系) 和registrationNameDependencies
(React 合成事件到原生事件的映射关系)。publishEventForPlugin(dispatchConfig, pluginModule, eventName) 函数
// packages\legacy-events\EventPluginRegistry.js function publishEventForPlugin( dispatchConfig: DispatchConfig, pluginModule: PluginModule<AnyNativeEvent>, eventName: string ): boolean { invariant( !eventNameDispatchConfigs.hasOwnProperty(eventName), 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName ) eventNameDispatchConfigs[eventName] = dispatchConfig const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames if (phasedRegistrationNames) { for (const phaseName in phasedRegistrationNames) { if (phasedRegistrationNames.hasOwnProperty(phaseName)) { const phasedRegistrationName = phasedRegistrationNames[phaseName] publishRegistrationName( phasedRegistrationName, pluginModule, eventName ) } } return true } else if (dispatchConfig.registrationName) { publishRegistrationName( dispatchConfig.registrationName, pluginModule, eventName ) return true } return false }
在 EventPluginRegistry
中,定义的全局对象:
registrationNameModule
全局对象:用于存储 React 合成事件与对应事件插件的映射关系。包含了 React 所支持的所有事件类型,在处理原生组件的props
时,可用于判断一个组件的prop
是否为事件类型,会根据不同的事件名称,找到对应的事件插件,然后统一绑定在document
上。{ onBlur: SimpleEventPlugin, onClick: SimpleEventPlugin, onClickCapture: SimpleEventPlugin, onChange: ChangeEventPlugin, onChangeCapture: ChangeEventPlugin, onMouseEnter: EnterLeaveEventPlugin, onMouseLeave: EnterLeaveEventPlugin, // ... }
registrationNameDependencies
全局对象:用于存储 React 合成事件到原生事件的映射关系。{ onBlur: ['blur'], onClick: ['click'], onClickCapture: ['click'], onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'], onMouseEnter: ['mouseout', 'mouseover'], onMouseLeave: ['mouseout', 'mouseover'], // ... }
plugins
全局对象:用于存储注册的所有插件列表,初始化为空。const plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
事件插件结构
export type DispatchConfig = {|
// 依赖的原生事件,即与之相关联的原生事件。注意,大多数事件一般只对应一个,复杂的事件会对应多个(如:onChange)
dependencies: Array<TopLevelType>,
// 对应的事件名称,React 会根据该参数查找对应的事件类型。
phasedRegistrationNames?: {|
bubbled: string, // 对应冒泡阶段
captured: string, // 对应捕获阶段
|},
// props事件注册名称,并不是所有的事件都具有冒泡事件的(比如:onMouseEnter),如果不支持冒泡,只会有 registrationName,而不会有 phasedRegistrationNames
registrationName?: string,
eventPriority: EventPriority, // 用来处理事件的优先级
|}
export type EventTypes = { [key: string]: DispatchConfig, ... }
export type PluginModule<NativeEvent> = {
eventTypes: EventTypes, // 声明插件的事件类型
// 事件进行处理的参数
extractEvents: (
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeTarget: NativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
container?: Document | Element | Node
) => ?ReactSyntheticEvent,
tapMoveThreshold?: number,
}
事件绑定流程
事件绑定处理逻辑
diffProperties
处理 React 合成事件在 React 的调和过程(Reconcilliation)中,通过 JSX 编译转换为 React Element 对象的每一个子节点都会形成一个与之对应的
fiber
对象。事件最终保存在fiber
中的memoizedProps
和pendingProps
中。React 在调合子节点后,进入 diff 阶段,会用 diff props 函数
diffProperties
单独处理。如果发现是 React 合成事件就会调用legacyListenToEvent
函数,注册事件监听器。diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) 函数
// packages\react-dom\src\client\ReactDOMComponent.js function ensureListeningTo( rootContainerElement: Element | Node, registrationName: string ): void { const isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE const doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument legacyListenToEvent(registrationName, doc) } // Calculate the diff between the two objects. export function diffProperties( domElement: Element, tag: string, lastRawProps: Object, nextRawProps: Object, rootContainerElement: Element | Document ): null | Array<mixed> { // ...... for (propKey in nextProps) { const nextProp = nextProps[propKey] const lastProp = lastProps != null ? lastProps[propKey] : undefined //...... if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { ensureListeningTo(rootContainerElement, propKey) } if (!updatePayload && lastProp !== nextProp) { // This is a special case. If any listener updates we need to ensure // that the "current" props pointer gets updated so we need a commit // to update this element. updatePayload = [] } } // ...... } // ...... }
legacyListenToEvent
注册事件监听器。在
legacyListenToEvent
函数中,- 首先,获取 React 合成事件对应的原生事件集合。比如:
onClick
对应[click]
;onChange
对应[blur , change , input , keydown , keyup]
。 - 然后,遍历依赖项的数组,调用
legacyListenToTopLevelEvent
绑定事件。- 基础事件(比如:
click
等),会默认按照事件冒泡处理(调用trapBubbledEvent
函数处理)。 - 特殊事件(比如:
scroll
、focus
、blur
等),会按照事件捕获处理(调用trapCapturedEvent
函数处理)。
- 基础事件(比如:
legacyListenToEvent(registrationName, mountAt) 函数
export function legacyListenToEvent( registrationName: string, // 合成事件名 mountAt: Document | Element | Node ): void { const listenerMap = getListenerMapForElement(mountAt) const dependencies = registrationNameDependencies[registrationName] for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i] // 合成事件所依赖的事件组 legacyListenToTopLevelEvent(dependency, mountAt, listenerMap) } } export function legacyListenToTopLevelEvent( topLevelType: DOMTopLevelEventType, mountAt: Document | Element | Node, listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)> ): void { if (!listenerMap.has(topLevelType)) { switch (topLevelType) { case TOP_SCROLL: trapCapturedEvent(TOP_SCROLL, mountAt) break case TOP_FOCUS: case TOP_BLUR: trapCapturedEvent(TOP_FOCUS, mountAt) trapCapturedEvent(TOP_BLUR, mountAt) // We set the flag for a single dependency later in this function, // but this ensures we mark both as attached rather than just one. listenerMap.set(TOP_BLUR, null) listenerMap.set(TOP_FOCUS, null) break case TOP_CANCEL: case TOP_CLOSE: if (isEventSupported(getRawEventName(topLevelType))) { trapCapturedEvent(topLevelType, mountAt) } break case TOP_INVALID: case TOP_SUBMIT: case TOP_RESET: // We listen to them on the target DOM elements. // Some of them bubble so we don't want them to fire twice. break default: // By default, listen on the top level to all non-media events. // Media events don't bubble so adding the listener wouldn't do anything. const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1 if (!isMediaEvent) { trapBubbledEvent(topLevelType, mountAt) } break } listenerMap.set(topLevelType, null) } }
trapBubbledEvent(topLevelType, element) 函数 / trapCapturedEvent(topLevelType, element) 函数
// packages\react-dom\src\events\ReactDOMEventListener.js export function trapBubbledEvent( topLevelType: DOMTopLevelEventType, element: Document | Element | Node ): void { trapEventForPluginEventSystem(element, topLevelType, false) } export function trapCapturedEvent( topLevelType: DOMTopLevelEventType, element: Document | Element | Node ): void { trapEventForPluginEventSystem(element, topLevelType, true) }
- 首先,获取 React 合成事件对应的原生事件集合。比如:
绑定事件统一处理函数
dispatchEvent
,进行事件监听。对合成事件进行冒泡/捕获处理,会调用
trapEventForPluginEventSystem()
函数。在函数中:- 判断事件类型,并绑定事件统一处理函数
dispatchEvent
,对listerner
进行赋值。 - 然后,判断是否为捕获,调用
addEventCaptureListener(container, rawEventName, listener)
或addEventBubbleListener(container, rawEventName, listener)
进行事件绑定。
实际上,所有的事件都绑定到
document
容器上。trapEventForPluginEventSystem(container, topLevelType, capture) 函数
// packages\react-dom\src\events\ReactDOMEventListener.js function trapEventForPluginEventSystem( container: Document | Element | Node, topLevelType: DOMTopLevelEventType, capture: boolean ): void { let listener switch (getEventPriorityForPluginSystem(topLevelType)) { case DiscreteEvent: listener = dispatchDiscreteEvent.bind( null, topLevelType, PLUGIN_EVENT_SYSTEM, container ) break case UserBlockingEvent: listener = dispatchUserBlockingUpdate.bind( null, topLevelType, PLUGIN_EVENT_SYSTEM, container ) break case ContinuousEvent: default: listener = dispatchEvent.bind( null, topLevelType, PLUGIN_EVENT_SYSTEM, container ) break } const rawEventName = getRawEventName(topLevelType) if (capture) { addEventCaptureListener(container, rawEventName, listener) } else { addEventBubbleListener(container, rawEventName, listener) } } // packages\react-dom\src\events\EventListener.js export function addEventBubbleListener( element: Document | Element | Node, eventType: string, listener: Function ): void { element.addEventListener(eventType, listener, false) } export function addEventCaptureListener( element: Document | Element | Node, eventType: string, listener: Function ): void { element.addEventListener(eventType, listener, true) }
- 判断事件类型,并绑定事件统一处理函数
事件绑定流程总结
- 在 React 中,将元素转换为与之对应的
fiber
对象,如果发现props
是合成事件(如:onClick
),则会按照事件系统逻辑进行处理 - 根据 React 合成事件类型,获取到对应的原生事件类型。比如:
onClick
对应[click]
;onChange
对应[blur , change , input , keydown , keyup]
。 - 判断原生事件类型,进行事件冒泡/事件捕获逻辑处理。大部分事件(如:
onClick
)都按照冒泡逻辑处理,少数事件(如:scroll
)会按照捕获逻辑处理。 - 调用
trapEventForPluginEventSystem()
函数进行事件绑定。事件绑定在document
上,并绑定事件统一处理函数dispatchEvent
。
注:并不是捕获事件就会走捕获的阶段(如:onClickCapture
),实际上,onClickCapture
与 onClick
一样,都是走的冒泡阶段。onScroll
、onBlur
、onFocus
等在事件捕获阶段发生的。
事件触发流程
事件触发处理逻辑
dispatchEvent
处理事件触发React 进行事件绑定的时候,会绑定事件统一处理函数
dispatchEvent
,进行事件监听。当触发事件之后,会执行dispatchEvent
函数。其中,会调用attemptToDispatchEvent()
函数调度事件。- 首先,根据事件源(即:
nativeEvent
),调用const nativeEventTarget = getEventTarget(nativeEvent)
找到真实 DOM 元素(即:nativeEventTarget
) - 然后,调用
let targetInst = getClosestInstanceFromNode(nativeEventTarget)
,根据该 DOM 元素,找到与之对应的fiber
,并赋值给targetInst
。React 在初始化真实 DOM 时,会使用一个随机的 keyinternalInstanceKey
指针指向当前 DOM 对应的 fiber 对象,fiber 对象用stateNode
指向了当前的 DOM 元素。 - 最后,执行
dispatchEventForLegacyPluginEventSystem
函数,进入 legacy 模式的事件处理函数系统
dispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) 函数
// packages\react-dom\src\events\ReactDOMEventListener.js export function dispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, container: Document | Element | Node, nativeEvent: AnyNativeEvent ): void { if (!_enabled) { return } if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) { // If we already have a queue of discrete events, and this is another discrete // event, then we can't dispatch it regardless of its target, since they // need to dispatch in order. queueDiscreteEvent( null, // Flags that we're not actually blocked on anything as far as we know. topLevelType, eventSystemFlags, container, nativeEvent ) return } const blockedOn = attemptToDispatchEvent( topLevelType, eventSystemFlags, container, nativeEvent ) if (blockedOn === null) { // We successfully dispatched this event. clearIfContinuousEvent(topLevelType, nativeEvent) return } if (isReplayableDiscreteEvent(topLevelType)) { // This this to be replayed later once the target is available. queueDiscreteEvent( blockedOn, topLevelType, eventSystemFlags, container, nativeEvent ) return } if ( queueIfContinuousEvent( blockedOn, topLevelType, eventSystemFlags, container, nativeEvent ) ) { return } // We need to clear only if we didn't queue because // queueing is accummulative. clearIfContinuousEvent(topLevelType, nativeEvent) // This is not replayable so we'll invoke it but without a target, // in case the event system needs to trace it. if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { dispatchEventForLegacyPluginEventSystem( topLevelType, eventSystemFlags, nativeEvent, null ) } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system DEPRECATED_dispatchEventForResponderEventSystem( (topLevelType: any), null, nativeEvent, getEventTarget(nativeEvent), eventSystemFlags ) } } else { dispatchEventForLegacyPluginEventSystem( topLevelType, eventSystemFlags, nativeEvent, null ) } } // Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked. export function attemptToDispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, container: Document | Element | Node, nativeEvent: AnyNativeEvent ): null | Container | SuspenseInstance { // TODO: Warn if _enabled is false. const nativeEventTarget = getEventTarget(nativeEvent) let targetInst = getClosestInstanceFromNode(nativeEventTarget) if (targetInst !== null) { let nearestMounted = getNearestMountedFiber(targetInst) if (nearestMounted === null) { // This tree has been unmounted already. Dispatch without a target. targetInst = null } else { const tag = nearestMounted.tag if (tag === SuspenseComponent) { let instance = getSuspenseInstanceFromFiber(nearestMounted) if (instance !== null) { // Queue the event to be replayed later. Abort dispatching since we // don't want this event dispatched twice through the event system. // TODO: If this is the first discrete event in the queue. Schedule an increased // priority for this boundary. return instance } // This shouldn't happen, something went wrong but to avoid blocking // the whole system, dispatch the event without a target. // TODO: Warn. targetInst = null } else if (tag === HostRoot) { const root: FiberRoot = nearestMounted.stateNode if (root.hydrate) { // If this happens during a replay something went wrong and it might block // the whole system. return getContainerFromFiber(nearestMounted) } targetInst = null } else if (nearestMounted !== targetInst) { // If we get an event (ex: img onload) before committing that // component's mount, ignore it for now (that is, treat it as if it was an // event on a non-React tree). We might also consider queueing events and // dispatching them after the mount. targetInst = null } } } if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { dispatchEventForLegacyPluginEventSystem( topLevelType, eventSystemFlags, nativeEvent, targetInst ) } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system DEPRECATED_dispatchEventForResponderEventSystem( (topLevelType: any), targetInst, nativeEvent, nativeEventTarget, eventSystemFlags ) } } else { dispatchEventForLegacyPluginEventSystem( topLevelType, eventSystemFlags, nativeEvent, targetInst ) } // We're not blocked on anything. return null }
- 首先,根据事件源(即:
legacy 事件处理系统与批量更新
在
dispatchEventForLegacyPluginEventSystem
函数中:首先,根据
getTopLevelCallbackBookKeeping
函数找到事件池中对应的属性,将topLevelType
,targetInst
等属性赋予给事件。然后,通过
batchedEventUpdates
函数处理批量更新。实际上,React 是通过
isBatchingEventUpdates
控制是否进行批量更新。batchedEventUpdates
会打开批量渲染开关(isBatchingEventUpdates
)并调用handleTopLevel
执行事件处理插件。事件的执行是在
handleTopLevel(bookKeeping)
中执行的。batchedEventUpdates(fn, a, b) 函数
// packages\legacy-events\ReactGenericBatching.js export function batchedEventUpdates(fn, a, b) { if (isBatchingEventUpdates) { // If we are currently inside another batch, we need to wait until it // fully completes before restoring state. return fn(a, b) } isBatchingEventUpdates = true try { return batchedEventUpdatesImpl(fn, a, b) } finally { isBatchingEventUpdates = false finishEventHandler() } }
在函数中触发
setState
,isBatchingEventUpdates
为true
,则具备了批量更新的能力。export default class ExampleComponent extends React.Component { state = { count: 0, } handleClick = () => { this.setState({ count: this.state.count + 1 }) console.log(this.state.count) // 0 setTimeout(() => { this.setState({ count: this.state.count + 1 }) console.log(this.state.count) // 2 }) } render() { return <button onClick={this.handleClick}>点击</button> } }
在示例中:
- 执行第一个
setState
:符合批量更新的条件,isBatchingEventUpdates
为true
,则打印的值不是最新数值(即:setState
为异步)。 - 执行第二个
setState
:在eventLoop
事件循环中,setTimeout
在下一次事件循环中执行,此时isBatchingEventUpdates
为false
,则能获取到最新值,打印为最新数值。
- 执行第一个
最终,通过
releaseTopLevelCallbackBookKeeping
来释放事件池。
dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) 函数
// packages\react-dom\src\events\DOMLegacyEventPluginSystem.js export function dispatchEventForLegacyPluginEventSystem( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber ): void { const bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, eventSystemFlags ) try { // Event queue being processed in the same cycle allows // `preventDefault`. batchedEventUpdates(handleTopLevel, bookKeeping) } finally { releaseTopLevelCallbackBookKeeping(bookKeeping) } }
执行事件插件函数
在进行事件处理系统与批量更新中,调用
handleTopLevel
函数主要是获取对应事件处理插件,比如onClick
对应SimpleEventPlugin
,调用流程:handleTopLevel
-->runExtractedPluginEventsInBatch
-->extractPluginEvents
-->runEventsInBatch
获取事件处理插件后,调用事件插件的处理函数
extractEvents
。通过extractEvents
可以不需要考虑浏览器兼容问题,交给 React 底层统一处理。handleTopLevel(bookKeeping) 函数
// packages\react-dom\src\events\DOMLegacyEventPluginSystem.js function extractPluginEvents( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags ): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null { let events = null for (let i = 0; i < plugins.length; i++) { // Not every plugin in the ordering may be loaded at runtime. const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i] if (possiblePlugin) { const extractedEvents = possiblePlugin.extractEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags ) if (extractedEvents) { events = accumulateInto(events, extractedEvents) } } } return events } function runExtractedPluginEventsInBatch( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags ) { const events = extractPluginEvents( topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags ) runEventsInBatch(events) } function handleTopLevel(bookKeeping: BookKeepingInstance) { let targetInst = bookKeeping.targetInst // Loop through the hierarchy, in case there's any nested components. // It's important that we build the array of ancestors before calling any // event handlers, because event handlers can modify the DOM, leading to // inconsistencies with ReactMount's node cache. See #1105. let ancestor = targetInst do { if (!ancestor) { const ancestors = bookKeeping.ancestors ;((ancestors: any): Array<Fiber | null>).push(ancestor) break } const root = findRootContainerNode(ancestor) if (!root) { break } const tag = ancestor.tag if (tag === HostComponent || tag === HostText) { bookKeeping.ancestors.push(ancestor) } ancestor = getClosestInstanceFromNode(root) } while (ancestor) for (let i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i] const eventTarget = getEventTarget(bookKeeping.nativeEvent) const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType) const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent) let eventSystemFlags = bookKeeping.eventSystemFlags // If this is the first ancestor, we mark it on the system flags if (i === 0) { eventSystemFlags |= IS_FIRST_ANCESTOR } runExtractedPluginEventsInBatch( topLevelType, targetInst, nativeEvent, eventTarget, eventSystemFlags ) } }
extractEvents
形成事件对象event
和事件处理函数队列- 首先,形成 React 合成事件源对象,保存整个事件的信息。将作为参数传递给真正的事件处理函数。
- 然后,声明事件执行队列,按照冒泡和捕获逻辑,从事件源开始逐渐向上,查找子节点与之对应的
fiber
对象 ,收集上面的 React 合成事件(例如:onClick
/onClickCapture
)。- 对于冒泡阶段的事件(如:
onClick
),将push
到执行队列后面 - 对于捕获阶段的事件(如:
onClickCapture
),将unShift
到执行队列的前面。
- 对于冒泡阶段的事件(如:
- 最后,将事件执行队列,保存到 React 事件源对象上,等待执行。
export default class ExampleComponent extends React.Component { handleDivClick = () => console.log('React冒泡:div元素') handleDivClickCapture = () => console.log('React捕获:div元素') handleButtonClick = () => console.log('React冒泡:button元素') handleButtonClickCapture = () => console.log('React捕获:button元素') render() { return ( <div id="div" onClick={this.handleDivClick} onClickCapture={this.handleDivClickCapture} > <button id="button" onClick={this.handleButtonClick} onClickCapture={this.handleButtonClickCapture} > 按钮 </button> </div> ) } } // 输出为: // React捕获:div元素 // React捕获:button元素 // React冒泡:button元素 // React冒泡:div元素
在示例中:
- 首先,遍历
button
对应的 fiber 。将onClickCapture
事件处理程序handleButtonClickCapture
,添加到事件队列最前面;将onClick
事件处理程序handleButtonClick
,添加到事件队列中。形成结构为[handleButtonClickCapture, handleButtonClick]
- 然后,向上遍历
div
对应的 fiber ,将onClickCapture
事件处理程序handleDivClickCapture
,添加到事件队列最前面;将onClick
事件处理程序handleDivClick
,添加到事件队列中。形成结构为[handleDivClickCapture, handleButtonClickCapture, handleButtonClick, handleDivClick]
事件触发
形成事件对象
event
和事件处理函数队列之后,通过runEventsInBatch
函数进行批量执行,触发事件。同时,如果发现有阻止冒泡,则会跳出循环,重置事件源。runEventsInBatch(events) 函数
// packages\legacy-events\EventBatching.js let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null const executeDispatchesAndRelease = function (event: ReactSyntheticEvent) { if (event) { executeDispatchesInOrder(event) if (!event.isPersistent()) { event.constructor.release(event) } } } const executeDispatchesAndReleaseTopLevel = function (e) { return executeDispatchesAndRelease(e) } export function runEventsInBatch( events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null ) { if (events !== null) { eventQueue = accumulateInto(eventQueue, events) } // Set `eventQueue` to null before processing it so that we can tell if more // events get enqueued while processing. const processingEventQueue = eventQueue eventQueue = null if (!processingEventQueue) { return } forEachAccumulated( processingEventQueue, executeDispatchesAndReleaseTopLevel ) invariant( !eventQueue, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.' ) // This would be a good time to rethrow if any of the event handlers threw. rethrowCaughtError() } // packages\legacy-events\forEachAccumulated.js function forEachAccumulated<T>( arr: ?(Array<T> | T), cb: (elem: T) => void, scope: ?any ) { if (Array.isArray(arr)) { arr.forEach(cb, scope) } else if (arr) { cb.call(scope, arr) } }
事件触发流程总结
- 首先通过统一事件处理函数
dispatchEvent
,进行批量更新batchedEventUpdates
- 根据事件源找到与之匹配的 DOM 元素
fiber
,执行事件对应的处理插件中的extractEvents
,并进行遍历,形成一个事件执行队列,React 使用该队列模拟事件捕获
-->事件源
-->事件冒泡
过程。 - 最后通过
runEventsInBatch
执行事件队列,完成整个触发流程。如果发现有阻止事件冒泡,则会跳出循环,重置事件源,放回到事件池中,完成整个流程。
React 17、18 事件系统
React 17、18 事件系统的相关调整:
- 事件统一绑定到
container
上 (ReactDOM.render(app, container)
) ,而不是document
上。有利于微前端,如果同时存在多个子应用,全部绑定在document
上,可能会出现问题。 - 对齐原生浏览器事件。React 17 中支持原生捕获事件,对齐了浏览器原生标准。同时,
onScroll
事件不再进行事件冒泡,onFocus
和onBlur
使用原生focusin
,focusout
合成。 - 取消事件池。React 17 取消事件池复用,也就解决了在 React 16 中,如果需要在事件处理函数运行之后获取事件对象的属性需要调用
e.persist()
的问题。
事件绑定流程
React 事件系统,在 createRoot
中,通过 listenToAllSupportedEvents
会向外层容器注册全部事件。
createRoot(container, options) 函数
// packages\react-dom\src\client\ReactDOMRoot.js
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.')
}
warnIfReactDOMContainerInDEV(container)
let isStrictMode = false
let concurrentUpdatesByDefaultOverride = false
let identifierPrefix = ''
let onRecoverableError = defaultOnRecoverableError
let transitionCallbacks = null
if (options !== null && options !== undefined) {
if (options.unstable_strictMode === true) {
isStrictMode = true
}
if (
allowConcurrentByDefault &&
options.unstable_concurrentUpdatesByDefault === true
) {
concurrentUpdatesByDefaultOverride = true
}
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError
}
if (options.transitionCallbacks !== undefined) {
transitionCallbacks = options.transitionCallbacks
}
}
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks
)
markContainerAsRoot(root.current, container)
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container
listenToAllSupportedEvents(rootContainerElement)
return new ReactDOMRoot(root)
}
listenToAllSupportedEvents
通过 listenToNativeEvent
绑定浏览器事件。
- 常规事件,则执行两次
listenToNativeEvent
,分别在冒泡和捕获阶段绑定事件。 - 不冒泡事件,则执行
listenToNativeEvent(domEventName, true, rootContainerElement)
。
在 listenToAllSupportedEvents(rootContainerElement)
函数中:
rootContainerElement
: 根节点root
。allNativeEvents
常量 :Set
集合,保存了 81 个浏览器常用事件。nonDelegatedEvents
常量 :Set
集合,保存了浏览器中不会冒泡的事件,一般指媒体事件(比如:pause
、play
、playing
等),还有一些特殊事件(比如:cancel
、close
、invalid
、load
、scroll
、toggle
)。
listenToAllSupportedEvents(rootContainerElement) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js
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') {
/* nonDelegatedEvents 保存了浏览器中不会冒泡的事件 */
if (!nonDelegatedEvents.has(domEventName)) {
/* 在冒泡阶段绑定事件 */
listenToNativeEvent(domEventName, false, rootContainerElement)
}
/* 在捕获阶段绑定事件 */
listenToNativeEvent(domEventName, true, rootContainerElement)
}
})
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!(ownerDocument: any)[listeningMarker]) {
;(ownerDocument: any)[listeningMarker] = true
listenToNativeEvent('selectionchange', false, ownerDocument)
}
}
}
}
listenToNativeEvent
本质上是向原生 DOM 中注册事件,进行事件监听。
在 listenToNativeEvent
中,调用 addTrappedEventListener
函数根据事件获取对应的优先级,不同的优先级在容器 DOM 节点注册不同的事件回调函数。
在 listenToNativeEvent(domEventName, isCapturePhaseListener, target)
函数中:
domEventName
入参对应的事件名(如:click
)isCapturePhaseListener
入参表示是否捕获:true
为捕获,false
为冒泡
listenToNativeEvent(domEventName, isCapturePhaseListener, target) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js
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
中,调用 createEventListenerWrapperWithPriority
函数判断事件执行的优先级,并返回对应的监听器。
在 createEventListenerWrapperWithPriority
函数中,根据 eventPriority
来判断优先级,不同的优先级返回不同的监听函数。
dispatchDiscreteEvent
:离散事件监听器,优先级为0
dispatchContinuousEvent
:用户阻塞事件监听器,优先级为1
dispatchEvent
:连续事件或其他事件监听器,优先级为2
addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) 函数
// 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
)
// If passive option is not supported, then the event will be
// active and not passive.
let isPassiveListener = undefined
if (passiveBrowserEventsSupported) {
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true
}
}
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer
let unsubscribeListener
// When legacyFBSupport is enabled, it's for when we
// want to add a one time event listener to a container.
// This should only be used with enableLegacyFBSupport
// due to requirement to provide compatibility with
// internal FB www event tooling. This works by removing
// the event listener as soon as it is invoked. We could
// also attempt to use the {once: true} param on
// addEventListener, but that requires support and some
// browsers do not support this today, and given this is
// to support legacy code patterns, it's likely they'll
// need support for such browsers.
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener
listener = function (...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener
)
return originalListener.apply(this, p)
}
}
// TODO: There are too many combinations here. Consolidate them.
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
)
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
)
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
)
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
)
}
}
}
createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) 函数
// packages\react-dom\src\events\ReactDOMEventListener.js
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
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer
)
}
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent
) {
const previousPriority = getCurrentUpdatePriority()
const prevTransition = ReactCurrentBatchConfig.transition
ReactCurrentBatchConfig.transition = null
try {
setCurrentUpdatePriority(DiscreteEventPriority)
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
} finally {
setCurrentUpdatePriority(previousPriority)
ReactCurrentBatchConfig.transition = prevTransition
}
}
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent
): void {
if (!_enabled) {
return
}
if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
)
} else {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
)
}
}
事件触发流程
触发事件时,首先会执行 dispatchEvent
函数,最终会通过 batchedUpdates
(批量更新) 来处理 dispatchEventsForPlugins
。
执行流程为:dispatchEvent
--> dispatchEventOriginal
/ dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
--> dispatchEventForPluginEventSystem
--> batchedUpdates
--> dispatchEventsForPlugins
dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) 函数
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent
): void {
if (!_enabled) {
return
}
if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
)
} else {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
)
}
}
在 dispatchEventsForPlugins
函数中:
- 相关入参
domEventName
:事件名称eventSystemFlags
:事件处理的阶段。(0
- 冒泡阶段,4
- 捕获阶段)nativeEvent
:原生事件的事件源(event
)targetInst
:DOM 元素对应的节点,即 fiber 节点targetContainer
:根节点
- 首先,通过
const nativeEventTarget = getEventTarget(nativeEvent)
获取到发生事件的元素,即事件源。 - 然后,创建事件队列
const dispatchQueue = []
,用于存储待更新的事件队列 - 接着,通过
extractEvents
收集事件 - 最后,通过
processDispatchQueue
执行事件
dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) 函数
// 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)
}
extractEvents
收集事件,以 SimpleEventPlugin.extractEvents
为例。在 SimpleEventPlugin
中的 extractEvents
中:
- 通过
topLevelEventsToReactNames.get(domEventName)
来获取对应的合成事件名称,如:onMouseOver
。 SyntheticEventCtor()
是合成函数的构造函数。- 然后,通过
switch case
来匹配对应的合成事件的构造函数。 inCapturePhase
判断是否捕获阶段。- 通过
accumulateSinglePhaseListeners()
函数来获取当前阶段的所有事件。在该函数中,会获取存储在 Fiber 上的 Props 的对应事件,然后通过createDispatchListener
返回的对象加入到监听集合上,如果是不会冒泡的函数则会停止(比如:scroll
),反之,则会向上递归。 - 最后,通过
new SyntheticEventCtor()
生成对应的事件源,插入队列中。
extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) 函数
// 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
) {
// TODO: we should remove the concept of a "SimpleEventPlugin".
// This is the basic functionality of the event system. All
// the other plugins are essentially polyfills. So the plugin
// should probably be inlined somewhere and have its logic
// be core the to event system. This would potentially allow
// us to ship builds of React without the polyfilled plugins below.
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
)
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0
// We don't process these events unless we are in the
// event's native "bubble" phase, which means that we're
// not in the capture phase. That's because we emulate
// the capture phase here still. This is a trade-off,
// because in an ideal world we would not emulate and use
// the phases properly, like we do with the SimpleEvent
// plugin. However, the plugins below either expect
// emulation (EnterLeave) or use state localized to that
// plugin (BeforeInput, Change, Select). The state in
// these modules complicates things, as you'll essentially
// get the case where the capture phase event might change
// state, only for the following bubble event to come in
// later and not trigger anything as the state now
// invalidates the heuristics of the event plugin. We
// could alter all these plugins to work in such ways, but
// that might cause other unknown side-effects that we
// can't foresee right now.
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
)
ChangeEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
)
SelectEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
)
BeforeInputEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
)
}
}
SimpleEventPlugin.extractEvents 函数
// 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
switch (domEventName) {
case 'keypress':
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
return
}
/* falls through */
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent
break
case 'focusin':
reactEventType = 'focus'
SyntheticEventCtor = SyntheticFocusEvent
break
case 'focusout':
reactEventType = 'blur'
SyntheticEventCtor = SyntheticFocusEvent
break
case 'beforeblur':
case 'afterblur':
SyntheticEventCtor = SyntheticFocusEvent
break
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return
}
/* falls through */
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup':
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent
break
case 'drag':
case 'dragend':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'dragstart':
case 'drop':
SyntheticEventCtor = SyntheticDragEvent
break
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent
break
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent
break
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent
break
case 'scroll':
SyntheticEventCtor = SyntheticUIEvent
break
case 'wheel':
SyntheticEventCtor = SyntheticWheelEvent
break
case 'copy':
case 'cut':
case 'paste':
SyntheticEventCtor = SyntheticClipboardEvent
break
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
case 'pointerdown':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerup':
SyntheticEventCtor = SyntheticPointerEvent
break
default:
// Unknown event. This is used by createEventHandle.
break
}
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0
if (
enableCreateEventHandleAPI &&
eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
) {
const listeners = accumulateEventHandleNonManagedNodeListeners(
// TODO: this cast may not make sense for events like
// "focus" where React listens to e.g. "focusin".
((reactEventType: any): DOMEventName),
targetContainer,
inCapturePhase
)
if (listeners.length > 0) {
// Intentionally create event lazily.
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
)
dispatchQueue.push({ event, listeners })
}
} else {
// Some events don't bubble in the browser.
// In the past, React has always bubbled them, but this can be surprising.
// We're going to try aligning closer to the browser behavior by not bubbling
// them in React either. We'll start by not bubbling onScroll, and then expand.
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'
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
)
if (listeners.length > 0) {
// Intentionally create event lazily.
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
)
dispatchQueue.push({ event, listeners })
}
}
}
processDispatchQueue
执行事件。在该函数中,遍历对应的合成事件,获取对应的事件源和监听的函数,最后会调用 processDispatchQueueItemsInOrder
函数。
在 processDispatchQueueItemsInOrde
函数中,通过 inCapturePhase
来模拟对应的冒泡与捕获。
event.isPropagationStopped()
:用于判断是否阻止冒泡(e.stopPropagation
),如果阻止冒泡,则会退出,从而模拟事件流的过程。executeDispatch()
:执行事件的函数。
processDispatchQueue(dispatchQueue, eventSystemFlags) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js
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)
// event system doesn't use pooling.
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError()
}
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
}
}
}