点击这里进入react原理专栏
这篇文章讲解以下react
的合成事件机制。一提到合成事件,有些人可能就会简单的认为,react合成事件就是为我们提供了一个兼容不同浏览器的事件对象。但其实合成事件机制并没有这么简单,比如看下面的代码输出(点击inner):
class App extends React.Component {
componentDidMount() {
outer.addEventListener('click', function() {
console.log('native event click outer')
})
inner.addEventListener('click', function() {
console.log('native event click inner')
})
}
handleClickInner = () => {
console.log('react event click inner')
}
handleClickOuter = () => {
console.log('react event click outer')
}
render() {
return (
<div className='outer' onClick={this.handleClickInner}>
<div className='inner' onClick={this.handleClickOuter}></div>
</div>
)
}
}
答案是:
native event click inner -> native event click outer -> react event click inner -> react event click outer
如果你不知道为什么是这个顺序的话,这篇文章会帮到你。
下面介绍一下react
的合成事件机制
总体介绍
首先,react
的合成事件机制绝不仅仅是封装了以兼容各个浏览器的事件对象这么简单,而是自己实现了一套事件触发机制。首先我们要明确两个概念:react
事件和原生事件。
react
事件就是我们在jsx
中写的类似onClick={this.handleClick}
这样的代码。而使用addEventListener
添加的事件就是原生事件。react
只会对react
事件采用合成事件机制来进行处理,而不会处理原生事件。
react
的事件处理分为两个阶段:初始渲染时的顶层注册,以及事件触发时的模拟派发。
顶层注册
react
合成事件机制采用了事件代理的思想,将每个react
事件依赖的原生事件绑定到了root
元素上。这里提到了react
事件依赖的原生事件。比如react
事件:onMouseEnter
,这个事件依赖了两个原生事件:mouseout
和mouseover
。react
会将所有react
事件依赖的原生事件都绑定到root
元素上,并且捕获阶段和冒泡阶段的事件都会绑定。可以在chrome
的开发者工具中看到root
元素上绑定的事件。
这些原生事件的挂载是在ReactDOM.render
中挂载的。ReactDOM.render
会调用createRootImpl
方法,该方法会调用listenToAllSupportedEvents
方法,循环所有事件,通过addTrappedEventListener
函数进行监听。
现在还有一个问题,监听函数listener
是什么?其实,root
元素上的事件监听函数就是一个触发器,触发器的创建也在addTrappedEventListener
函数中
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
// ...
}
createEventListenerWrapperWithPriority
从名字也可以看出,这个方法创建了一个带有优先级的触发器
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
var eventPriority = getEventPriorityForPluginSystem(domEventName);
var listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
而不论是那种优先级,最终都会调用dispatchEvent
函数,因此这个函数就是最终的触发器。
这样,react
就完成了事件的顶层注册。当我们触发点击事件时,会进入模拟派发阶段。
模拟派发
如果我们触发了一个click
事件,根据DOM事件流,首先在事件捕获阶段,root
元素的click
事件绑定的捕获阶段监听函数会被触发,也就是dispatchEvent
函数。dispatchEvent
函数会根据event.target
属性确定事件源对象,并收集从源对象到fiber
根节点的所有dom节点以及绑定的事件处理函数,之后一次调用这些事件处理函数,这样就模拟了事件捕获阶段的触发过程。
而在事件冒泡阶段,执行流程类似,当事件冒泡到root
元素时,触发dispatchEvent
函数,之后收集事件处理函数,并依次执行。
这里要明确一点:如果我们在某个
div
上绑定了onClick
事件,react
并不会把事件绑定到这个div
上,而是调用这个div
的事件处理函数来模拟事件的触发。
知道了大致流程后,我们来看一下源码
dispatchEvent
最终会调用dispatchEventForPluginEventSystem
函数
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
// ...
batchedEventUpdates(function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
}
这里的batchedEventUpdates
暂时不看,这涉及到state
更新相关的内容,它会调用dispatchEventsForPlugins
函数。
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];
// 事件路径收集
extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
// 事件模拟
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents$5
方法会调用extractEvents$4
function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
// ...
var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
if (_listeners.length > 0) {
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners
});
}
}
accumulateSinglePhaseListeners
就会收集事件触发路径,并返回收集到的事件处理函数数组。之后创建一个合成事件对象。这也就是为什么react
官方文档中说所有事件共享一个事件对象。
接下来看一下accumulateSinglePhaseListeners
function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly) {
var captureName = reactName !== null ? reactName + 'Capture' : null;
var reactEventName = inCapturePhase ? captureName : reactName;
var listeners = [];
var instance = targetFiber;
var lastHostComponent = null;
while (instance !== null) {
var _instance2 = instance,
stateNode = _instance2.stateNode,
tag = _instance2.tag;
// HostComponent就是浏览器原生标签,比如div,button
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
if (reactEventName !== null) {
var listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(createDispatchListener(instance, listener, lastHostComponent));
}
}
}
if (accumulateTargetOnly) {
break;
}
instance = instance.return;
}
return listeners;
}
这个方法主体就是一个循环,从事件源fiber
开始向上查找,如果遇到了HostComponent
,收集对应fiber
对象的事件监听函数,放入listeners
数组。注意,这个方法只会收集捕获和冒泡阶段两者中一个阶段的事件处理函数。捕获阶段触发dispatchEvent
之后收集捕获阶段的事件函数,冒泡阶段触发dispatchEvent
收集冒泡阶段额事件函数。
事件函数收集完毕后,开始模拟触发,调用processDispatchQueue
方法,该方法会调用processDispatchQueueItemsInOrder
来循环listeners
数组。
function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
var previousInstance;
if (inCapturePhase) {
for (var i = dispatchListeners.length - 1; i >= 0; i--) {
var _dispatchListeners$i = dispatchListeners[i],
instance = _dispatchListeners$i.instance,
currentTarget = _dispatchListeners$i.currentTarget,
listener = _dispatchListeners$i.listener;
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (var _i = 0; _i < dispatchListeners.length; _i++) {
var _dispatchListeners$_i = dispatchListeners[_i],
_instance = _dispatchListeners$_i.instance,
_currentTarget = _dispatchListeners$_i.currentTarget,
_listener = _dispatchListeners$_i.listener;
if (_instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, _listener, _currentTarget);
previousInstance = _instance;
}
}
}
这个方法也是一个循环。注意我们的listeners
数组中函数的存放顺序,是从事件源对象到顶层fiber
,这和冒泡阶段的顺序是对应的。因此,如果当前阶段是捕获阶段,那么反向遍历该数组即可,如果是冒泡阶段,就正向遍历。
executeDispatch
方法就会执行执行事件回调函数。
这里再次提醒,对于模拟派发的流程,会在事件捕获阶段执行一次,在冒泡阶段再执行一次。
举个🌰
回到文章开篇的例子,点击inner
,输出结果是什么呢?
class App extends React.Component {
componentDidMount() {
outer.addEventListener('click', function() {
console.log('native event click outer')
})
inner.addEventListener('click', function() {
console.log('native event click inner')
})
}
handleClickInner = () => {
console.log('react event click inner')
}
handleClickOuter = () => {
console.log('react event click outer')
}
render() {
return (
<div className='outer' onClick={this.handleClickInner}>
<div className='inner' onClick={this.handleClickOuter}></div>
</div>
)
}
}
一步步分析一下:初次渲染时,react
事件和原生事件都被注册。当点击inner
时,首先进入事件捕获阶段,触发root
元素的click
事件,触发捕获阶段的dispatchEvent
,但是发现没有绑定捕获阶段的监听函数,因此直接跳过。
接下来进入目标阶段,触发inner
的原生click
事件,输出native event click inner
。
之后进入冒泡阶段,来到outer
,触发outer
的原生事件,输出native event click outer
。之后冒泡到root
元素,开始收集事件触发路径,收集到的listeners
数组为[handleClickInner, handleClickOuter]
。因为时冒泡阶段,因此正序遍历数组,输出为react event click inner
, react event click outer
。
因此答案是:native event click inner -> native event click outer -> react event click inner -> react event click outer
总结
这篇文章讲解了react
事件机制的两个主体流程,顶层注册和模拟派发。相信大家也能够理解模拟
两个字的含义了。文章中有一个函数batchedEventUpdates
没有讲到,这涉及到了状态更新的内容,之后会有一篇文章专门将事件触发和状态更新结合。