事件系统
事件系统
原生 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 SimpleEventPluginEventConstructor.getPooled 实际调用为 SyntheticEvent 中的 getPooledEvent 方法,当 EventConstructor.eventPool 存在时会复用事件对象,否则会创建新的对象。
在 getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) 方法中:
dispatchConfig:该参数将事件对应的 React 元素实例、原生事件、原生事件对应的 DOM 封装成了一个合成事件。比如,冒泡事件中的onClick和捕获事件中的onClickCapturetargetInst:组件的实例,通过e.target(事件源) 得到对应的ReactDomComponentnativeEvent:对应原生事件对象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 的调和过程(Reconciliation)中,通过 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,对listener进行赋值。 - 然后,判断是否为捕获,调用
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 17/18 事件绑定流程
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:离散事件监听器,优先级为0dispatchContinuousEvent:用户阻塞事件监听器,优先级为1dispatchEvent:连续事件或其他事件监听器,优先级为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
)
}
}React 17/18 事件触发流程
触发事件时,首先会执行 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 函数。
在 processDispatchQueueItemsInOrder 函数中,通过 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
}
}
}