基础-合成事件
事件委托
在js中, 事件委托(Event Delegation)是一种事件响应机制, 当需要监听不存在的元素或者是动态生成元素的时候, 就可以考虑事件委托.
事件委托得益于事件冒泡, 当监听子元素的时候, 事件冒泡会通过目标元素向上传递到父级, 直到document, 如果子元素不确定或者动态生成, 可以通过监听父元素来取代监听子元素.
事件委托可以通过监听父级来达到监听子级的效果, 减少监听的数量, 使用更少的内存. 可以随意的对子元素进行事件绑定也不用担心忘记解绑.
React中不但利用了这套机制, 还自身实现了一套自己的事件机制, 消除了不同浏览器之间的差异.
React实现了一个合成事件层, 利用这个事件层把IE和W3C标准之间的兼容问题消除了.
合成事件和原生事件
- 原生事件: 在
componentDidMount中进行addEventListener绑定的事件 - 合成事件: 通过JSX方式绑定的时间, 比如
onClick={()=> this.handle()}
比如这段代码中:
{
this.state.showBox && <div onClick={e => e.stopPropagation()}>我是弹窗</div>
}
然后在生命周期中对body进行绑定:
componentDidMount() {
document.body.addEventListener('click', this.handleClickBody, false)
}
componentWillUnmount() {
document.body.removeEventListener('click', this.handleClickBody, false)
}
因为合成事件的触发时基于浏览器的事件机制实现的, 通过冒泡机制到最顶层元素, 然后再由dispatchEvent统一处理.

浏览器的事件执行需要经过三个阶段: 捕获阶段-目标元素阶段-冒泡阶段
此时对于合成事件进行阻止, 原生事件会执行吗?
会, 因为原生事件先于合成事件执行.
合成事件特点
- React 注册的事件最终会绑定到document这个dom上, 而不是react组件对应的dom(减少内存开销就是因为所有的事件都绑定在了document上, 其他的节点没有绑定事件)
- React 自身实现了一套事件冒泡机制, 所以我们这里的
e.stopPropagation()无效的原因. - React 通过队列的形式, 从触发的组件向父组件回溯, 然后调用他们JSX中定义的callback.
- React 有一套自己的合成事件
SyntheticEvent, 不是原生的 - React 通过对象池的形式管理合成事件对象的创建和销毁, 减少了垃圾的生成和新对象内存的分配, 提高了性能.
React 事件系统
事件系统的源码地址在这里
大概的合成系统框架如下:
/**
* React和事件系统概述:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
*/
简单的来说:
Top-level delegation用于捕获最原始的浏览器事件, 它主要由ReactEventListener负责,ReactEventListener被注入后可以支持插件化的事件源, 这一过程发生在主线程.- React对事件进行规范化和重复数据删除, 以解决浏览器的差异性
- 将这些本地事件转发到
EventPluginHub, 后者将询问插件是否要提取任何事件 EventPluginPub将为每个事件添加dispatches来对其进行注释和处理EventPluginHub会调度分派事件.
从名称可以看出:
- ReactEventListener: 负责事件的注册
- ReactEventEmitter: 负责事件的分发
- EventPluginHub: 负责事件的存储以及分发
- Plugin:根据不同的事件类型构造不同的合成事件
下面就分别看看它是如何工作的
事件注册
React中注册一个事件非常简单, 比如:
class TaskEvent extends Reac.PureComponent {
render() {
return (
<div
onClick={() => {
console.log('我是注册事件')
}}
>
呵呵呵
</div>
)
}
}
组件在创建mountComponent和更新updateComponent的时候, 都会调用_updateDOMProperties()方法.
mountComponent: function(transaction, hostParent, hostContainerInfo, context) {
// ...
var props = this._currentElement.props;
// ...
this._updateDOMProperties(null, props, transaction);
// ...
}
在这个方法中:
_updateDOMProperties: function (lastProps, nextProps, transaction) {
// ...
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined;
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
continue;
}
if (propKey === STYLE) {
// ...
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// 如果是props这个对象直接声明的属性,而不是从原型链中继承而来的,则处理它
// 对于mountComponent,lastProp为null。updateComponent二者都不为null。unmountComponent则nextProp为null
if (nextProp) {
// mountComponent和updateComponent中,enqueuePutListener注册事件
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
// unmountComponent中,删除注册的listener,防止内存泄漏
deleteListener(this, propKey);
}
}
}
}
enqueuePutListener
在这个方法中, 会通过enqueuePutListener()方法进行注册事件, 我们接着看看这个方法内部的执行逻辑:
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return
}
var containerInfo = inst._hostContainerInfo
var isDocumentFragment =
containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE
// 找到document
var doc = isDocumentFragment
? containerInfo._node
: containerInfo._ownerDocument
// 注册事件,将事件注册到document上
listenTo(registrationName, doc)
// 存储事件,放入事务队列中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
})
}
这个方法主要做了两件事:
- 通过调用
listenTo把时间注册到document上 - 事务方式调用
putListener存储事件(就是把React组件内的所有事件统一的存放到一个对象里面, 缓存起来, 为了在出发事件的时候可以查到对应的方法去执行)
那么listenTo()中是如何做的呢?
listenTo
export function listenTo(
registrationName: string,
mountAt: Document | Element | Node
): void {
const listeningSet = getListeningSetForElement(mountAt)
const dependencies = registrationNameDependencies[registrationName]
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i]
// 调用该方法进行注册
listenToTopLevel(dependency, mountAt, listeningSet)
}
}
registerationName就是传递过来的onClick, 而变量registrationNameDependencies是一个存储了React事件名和浏览器原生事件名对应的一个Map, 可以通过这个map拿到响应的浏览器原生事件名称.
export function listenToTopLevel(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listeningSet: Set<DOMTopLevelEventType | string>
): void {
if (!listeningSet.has(topLevelType)) {
switch (topLevelType) {
//...
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt) // 捕获阶段
}
break
default:
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt) // 冒泡阶段
}
break
}
listeningSet.add(topLevelType)
}
}
忽略其他不重要的源码 , 我们看到注册事件的入口是listenTo方法, 通过对dependencies循环调用listenToTopLevel方法, 在该方法中调用 trapCapturedEvent 和 trapBubbledEvent 来注册捕获和冒泡事件.
trapCapturedEvent 和 trapBubbledEvent
我们对trapCapturedEvent进行分析:
// 捕获阶段
export function trapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element | Node
): void {
trapEventForPluginEventSystem(element, topLevelType, true)
}
// 冒泡阶段
export function trapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element | Node
): void {
trapEventForPluginEventSystem(element, topLevelType, false)
}
发现两者都是调用如下方法:
function trapEventForPluginEventSystem(
element: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
capture: boolean // 决定捕获还是冒泡阶段
): void {
let listener
switch (getEventPriority(topLevelType)) {
}
const rawEventName = getRawEventName(topLevelType)
if (capture) {
// 捕获事件
addEventCaptureListener(element, rawEventName, listener)
} else {
// 冒泡事件
addEventBubbleListener(element, rawEventName, listener)
}
}
这里我们就能知道, 捕获事件通过addEventCaptureListener(), 而冒泡事件通过addEventBubbleListener().
// 捕获
export function addEventCaptureListener(
element: Document | Element | Node,
eventType: string,
listener: Function
): void {
element.addEventListener(eventType, listener, true)
}
// 冒泡
export function addEventBubbleListener(
element: Document | Element | Node,
eventType: string,
listener: Function
): void {
element.addEventListener(eventType, listener, false)
}
到这里, 事件的注册流程就完成了.
下面就是把注册的时间存储起来:
事件存储
在上面的enqueuePutListener()方法中, 我们把事件放入到事务队列中:
function enqueuePutListener(inst, registrationName, listener, transaction) {
//...
// 注册事件,将事件注册到document上
listenTo(registrationName, doc)
// 存储事件,放入事务队列中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
})
}
其中的putListener, 实现了事件的存储:
putListener: function (inst, registrationName, listener) {
// 用来标识注册了事件,比如onClick的React对象。key的格式为'.nodeId', 只用知道它可以标示哪个React对象就可以了
// step1: 得到组件唯一标识
var key = getDictionaryKey(inst);
// step2: 得到listenerBank对象中指定事件类型的对象
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
// step3: 将listener事件回调方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId]
// 所有React组件对象定义的所有React事件都会存储在listenerBank中
bankForRegistrationName[key] = listener;
// ...
}
// 拿到组件唯一标识
var getDictionaryKey = function (inst) {
return '.' + inst._rootNodeID;
};
事件分发
既然事件已经委托注册到document上了, 那么事件触发的时候, 肯定需要一个事件分发的过程, 流程也很简单, 既然事件存储在listenerBank, 那么我就只需要找到对应的事件类型, 然后执行事件回调就可以了.
注意, 由于元素本身没有注册任何事件, 而是委托到了document上, 所以这个将被触发的事件是React自带的合成事件, 而非浏览器原生事件.
首先找到事件触发的DOM和React Component, 找真实的DOM还是比较好找的, 这部分代码在getEventTag中:
// 源码看这里: https://github.com/facebook/react/blob/master/packages/react-dom/src/events/ReactDOMEventListener.js#L419
const nativeEventTarget = getEventTarget(nativeEvent)
let targetInst = getClosestInstanceFromNode(nativeEventTarget)
// ...
function getEventTarget(nativeEvent) {
let target = nativeEvent.target || nativeEvent.srcElement || window
if (target.correspondingUseElement) {
target = target.correspondingUseElement
}
return target.nodeType === TEXT_NODE ? target.parentNode : target
}
这个nativeEventTarget对象上挂载了一个以__reactInernalInstance开头的属性, 这个属性就是internalInstanceKey, 其值就是当前React实例对应的React Component.
function dispatchEventForPluginEventSystem(
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)
}
}
batchedEventUpdates()批量更新, 它的工作是把当前触发的事件放在了批处理队列中. handleTopLevel是事件分发的核心所在.
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)
}
这里看注释, 说的是: 事件回调可能会改变DOM结构, 所以要先遍历层理结构, 以防存在任何嵌套的组件, 然后缓存起来.
然后继续执行这个方法:
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i]
// getEventTarget上边有讲到
const eventTarget = getEventTarget(bookKeeping.nativeEvent)
const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType)
const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent)
runExtractedPluginEventsInBatch(
topLevelType,
targetInst,
nativeEvent,
eventTarget,
bookKeeping.eventSystemFlags
)
}
这里用了一个for循环来比阿尼这个React Component及其所有的父组件, 然后执行runExtractedPluginEventsInBatch方法.
从这里可以看出, React自身实现了一套冒泡机制, 从触发事件的对象开始, 向父元素回溯, 一次调用他们注册的时间的callback.
事件执行
上面我们讲到runExtractedPluginEventsInBatch就是事件执行的入口, 通过源码, 他主要做了两件事:
- 构造合成事件
- 批处理构造出来的合成事件
export function runExtractedPluginEventsInBatch(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
eventSystemFlags: EventSystemFlags
) {
// step1 : 构造合成事件
const events = extractPluginEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
)
// step2 : 批处理
runEventsInBatch(events)
}
构造合成事件
相关的代码在extractPluginEvents()和runEventsInBatch()中
function extractPluginEvents(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: 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
}
首先回去遍历plugins, 这个plugins就是所有事件合成plugins的集合数据, 这些plugins是在EventPluginHub初始化时候注入的.
// 📢 源码地址 : https://github.com/facebook/react/blob/master/packages/legacy-events/EventPluginHub.js#L80
export const injection = {
injectEventPluginOrder,
injectEventPluginsByName
}
// 📢 源码地址 : https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMClientInjection.js#L26
EventPluginHubInjection.injectEventPluginOrder(DOMEventPluginOrder)
EventPluginHubInjection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin
})
我们继续看extractEvent的逻辑代码:
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
)
if (extractedEvents) {
events = accumulateInto(events, extractedEvents)
}
因为const possiblePlugin: PluginModule = plugins[i], 类型是PluginModule, 我们可以去 SimpleEventPlugin 源码去看一下 extractEvents 到底干了啥:
extractEvents: function() {
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]
if (!dispatchConfig) {
return null
}
//...
}
首先, 看下topLevelEventsToDispatchConfig这个对象中有没有topLevelType这个属性, 只要有, 那么说明当前事件可以使用SimpleEventPlugin构造合成事件.
函数中顶一个EventConstructor, 然后通过switch..case进行赋值:
extractEvents: function() {
//...
let EventConstructor
switch (topLevelType) {
// ...
case DOMTopLevelEventTypes.TOP_POINTER_UP:
EventConstructor = SyntheticPointerEvent
break
default:
EventConstructor = SyntheticEvent
break
}
}
总之赋值给EventConstructor.
设置好了EventConstructor之后, 这个方法继续执行:
extractEvents: function() {
//...
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget
)
accumulateTwoPhaseDispatches(event)
return event
}
这段代码的意思是: 从event对象池中取出合成事件, 这里的getPooled()方法, 其实在SyntheticEvent初始化的时候就被设置好了:
function addEventPoolingTo(EventConstructor) {
EventConstructor.eventPool = []
// 就是这里设置了getPooled
EventConstructor.getPooled = getPooledEvent
EventConstructor.release = releasePooledEvent
}
SyntheticEvent.extend = function(Interface) {
//...
addEventPoolingTo(Class)
return Class
}
addEventPoolingTo(SyntheticEvent)
看到这里, 我们知道getPooled就是getPooledEvent:
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
)
}
首先, 会先到对象池中, 看一下length是否为0, 如果是第一次事件触发, 那需要new EventConstructor, 如果后续再次触发事件, 则直接从对象池中取, 也就是直接instance = EventConstructor.eventPool.pop()就可以了.
事件执行还有另一个重要操作: 批处理 runEventsInBatch(events)
批处理
批处理主要是通过 runEventQueueInBatch(events) 进行的:
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()
}
这个方法首先将当前需要处理的events事件, 与之前没有处理完毕的队列调用accumulateInto方法按照顺序进行合并, 组成一个新的队列
如果processingEventQueue这个为空, 则表示没有处理的事件, 退出, 否则调用forEacgAccumulated():
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)
}
}
这个方法就是看下事件队列是不是一个数组, 如果是, 则说明需要遍历队列, 调用executeDispatchesAndReleaseTopLevel方法, 否则说明队列中只有一个事件, 则无需遍历直接调用即可.
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 executeDispatchesInOrder(event) {
const dispatchListeners = event._dispatchListeners
const dispatchInstances = event._dispatchInstances
if (__DEV__) {
validateEventDispatches(event)
}
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break
}
// Listeners and Instances are two parallel arrays that are always in sync.
executeDispatch(event, dispatchListeners[i], dispatchInstances[i])
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances)
}
event._dispatchListeners = null
event._dispatchInstances = null
}
首先拿到事件上挂载的dispatchListeners, 就是所有注册事件回调函数的集合, 遍历这个集合, 如果event.isPropagationStopped() = ture, 那就直接退出, 说明在此之前触发的事件已经调用了event.stopPropagation(), isPropagationStopped的值被置为true, 当前事件以及后面的事件作为父级事件就不应该再被执行了.
这里当event.isPropagationStopped()为true时,中断合成事件的向上遍历执行,也就起到了和原生事件调用 stopPropagation相同的效果 如果循环没有被中断,则继续执行executeDispatch方法.