合成事件:
- 注册
- 触发
- 合成事件和原生事件的执行顺序
注册
React 17 将事件委托放在了 root 上而不是以前的 document 上。这个是一个垫脚石的功能,这样做有一个原因是:当同个项目里,有多个 React 根节点时(也可能是React多版本共存),避免可能的一些操作(如阻止冒泡)会影响到其他 React 节点的正常工作。
当 ReactDOM.render 时,将会在创建根节点 Fiber 时(createRootImpl),对所有可监听的事件进行注册(listenToAllSupportedEvents)。
触发
我们在 React 元素上设置事件时,如:
<div onClick={() => setB(1234)}>{b}</div>
当点击该元素时,且处于冒泡模式时:
元素会冒泡到根节点,根节点获取事件源后,会找到对应的 Fiber,然后遍历获取节点的父节点去收集事件直到根节点 ,然后按照顺序地执行。
当处于捕获模式时:
根节点会捕获到事件源,然后会找到对应的 Fiber,然后遍历获取节点的父节点去收集事件直到根节点 ,然后按照顺序地执行。
获取节点的父节点去收集该父节点事件的流程大概如下:
dispatchDiscreteEvent ->
discreteUpdates ->
discreteUpdatesImpl ->
dispatchEvent ->
attemptToDispatchEvent ->
dispatchEventForPluginEventSystem ->
batchedEventUpdates ->
batchedEventUpdatesImpl ->
dispatchEventsForPlugins ->
extractEvents$5
(
extractEvents$4 -> accumulateSinglePhaseListeners ==>
dispatchQueue.push({
event: _event,
listeners: _listeners
})
) ->
processDispatchQueue
(
processDispatchQueueItemsInOrder -> executeDispatch -> invokeGuardedCallbackAndCatchFirstError -> invokeGuardedCallback -> invokeGuardedCallbackProd -> func.apply(context, funcArgs)
)
流程很长,但最主要的两个方法就是 extractEvents$4 和 processDispatchQueue ,前者是收集该节点及该节点到根节点之间的事件,将它们保存在 dispatchQueue 里,然后调用 processDispatchQueue 来依次执行里面的方法。
合成事件和原生事件的执行顺序
这里有个点,就是合成事件和原生事件的执行顺序。React 17 版本以上 和 React 16.14.0 版本有些不一样(捕获模式)。看下代码:
// 代码改造于 https://segmentfault.com/a/1190000038251163
import {createRef, Component} from 'react';
class App extends Component {
parentRef;
childRef;
constructor(props) {
super(props);
this.parentRef = createRef();
this.childRef = createRef();
}
componentDidMount() {
console.log("React componentDidMount!");
this.parentRef.current?.addEventListener("click", () => {
console.log("原生事件:父元素 DOM 事件监听!");
});
this.childRef.current?.addEventListener("click", () => {
console.log("原生事件:子元素 DOM 事件监听!");
});
document.addEventListener("click", (e) => {
console.log("原生事件:document DOM 事件监听!");
});
}
parentClickFun = () => {
console.log("React 事件:父元素事件监听!");
};
childClickFun = () => {
console.log("React 事件:子元素事件监听!");
};
render() {
return (
<div ref={this.parentRef} onClick={this.parentClickFun}>
<div ref={this.childRef} onClick={this.childClickFun}>
分析事件执行顺序
</div>
</div>
);
}
}
export default App;
按照冒泡模式来说,原生事件(子 -> 父) > 合成事件(子 -> 父) > document 原生事件。因为合成事件需要冒泡到根节点后才进行处理,而原生事件可以即时执行。所以执行结果是:
// V17.0.2 和 V16.14.0 一致
// 原生事件:子元素 DOM 事件监听!
// 原生事件:父元素 DOM 事件监听!
// React 事件:子元素事件监听!
// React 事件:父元素事件监听!
// 原生事件:document DOM 事件监听!
那按照捕获模式来说呢?改造一下代码:
// 忽略无关代码
this.parentRef.current?.addEventListener("click", () => {
console.log("原生事件:父元素 DOM 事件监听!");
}, true);
this.childRef.current?.addEventListener("click", () => {
console.log("原生事件:子元素 DOM 事件监听!");
}, true);
document.addEventListener("click", (e) => {
console.log("原生事件:document DOM 事件监听!");
}, true);
(
<div ref={this.parentRef} onClickCapture={this.parentClickFun}>
<div ref={this.childRef} onClickCapture={this.childClickFun}>
分析事件执行顺序
</div>
</div>
);
那结果是怎样的呢?请看:
// V17.0.2
// 原生事件:document DOM 事件监听!
// React 事件:父元素事件监听!
// React 事件:子元素事件监听!
// 原生事件:父元素 DOM 事件监听!
// 原生事件:子元素 DOM 事件监听!
// V16.14.0
// 原生事件:document DOM 事件监听!
// 原生事件:父元素 DOM 事件监听!
// 原生事件:子元素 DOM 事件监听!
// React 事件:父元素事件监听!
// React 事件:子元素事件监听!
根据结果来看,当处于捕获模式时,V17.0.2 版本 document 原生事件 > 合成事件(父 -> 子) > 原生事件(父 -> 子),而 V16.14.0 版本 document 原生事件 > 原生事件(父 -> 子)> 合成事件(父 -> 子)。这是一个细微的改动:
V17 之前,合成事件和原生事件的执行顺序与冒泡/捕获模式无关,原生事件恒早于合成事件;
V17 后,合成事件和原生事件的执行顺序与冒泡/捕获模式相关,冒泡模式,原生事件早于合成事件,捕获模式,合成事件早于原生事件。
为什么呢?
且看 V17 版本之前,部分关键代码如下:
// listenTo
export function listenTo(
registrationName: string,
mountAt: Document | Element,
) {
const isListening = getListeningForDocument(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
switch (dependency) {
// 忽略无关代码
default:
const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1
if (!isMediaEvent) {
// onClick 和 onClickCapture 都使用 trapBubbledEvent
trapBubbledEvent(dependency, mountAt)
}
break;
}
}
}
}
function trapBubbledEvent(topLevelType, element) {
// 忽略无关代码
addEventBubbleListener(element, getRawEventName(topLevelType)
}
function addEventBubbleListener(element, eventType, listener) {
element.addEventListener(eventType, listener, false);
}
已知 onClickCapture 和 onClick 都使用冒泡模式进行监听,那当使用 onClickCapture 时,怎么处理才符合捕获模式时的调用结果呢?
其实是通过 traverseTwoPhase 方法遍历模拟捕获和冒泡,实质是调用了 accumulateTwoPhaseDispatchesSingle 方法 ,该方法内通过 listenerAtPhase 方法里的 getListener 来获取到对应元素上设置的对应的事件函数,然后保存在 event._dispatchListeners 里:
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
var i = void 0;
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
function accumulateDirectionalDispatches(inst, phase, event) {
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
function listenerAtPhase(inst, event, propagationPhase) {
var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
// registrationName 就是 eventType 如:onClick / onClickCapture
return getListener(inst, registrationName);
}
// 获取对应元素上设置的 onClick / onClickCapture 方法
function getListener(inst, registrationName) {
var listener = void 0;
var stateNode = inst.stateNode;
if (!stateNode) {
return null;
}
var props = getFiberCurrentPropsFromNode(stateNode);
if (!props) {
return null;
}
listener = props[registrationName];
return listener;
}
当执行 executeDispatchesInOrder 方法时,会遍历执行 event._dispatchListeners 里保存的事件函数。
/**
* Standard/simple iteration through an event's collected dispatches.
*/
function executeDispatchesInOrder(event) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances);
}
}
所以,V17 版本前的捕获模式,只是模拟的,当事件冒泡到 document 时,开始处理,通过遍历该元素以及元素父节点直到根节点,模拟捕获和冒泡处理方式,获取对应模式下的事件函数,然后调用它们。
所以,在 V17 版本前,原生事件的执行时机是恒早于合成事件的执行时机的。
// V16.14.0
// 原生事件:document DOM 事件监听!
// 原生事件:父元素 DOM 事件监听!
// 原生事件:子元素 DOM 事件监听!
// React 事件:父元素事件监听!
// React 事件:子元素事件监听!
那 V17 版本后,发生了哪些变化呢?导致结果跟 V17 版本之前的不一样了呢?
// V17.0.2
// 原生事件:document DOM 事件监听!
// React 事件:父元素事件监听!
// React 事件:子元素事件监听!
// 原生事件:父元素 DOM 事件监听!
// 原生事件:子元素 DOM 事件监听!
其实差别就在于,V17 版本之前,捕获仅是模拟,实质还是冒泡到 document 后再进行对应事件的处理,而 V17 后,捕获事件将会启用捕获模式的监听:
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
// 忽略无关代码
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
}
}
function addEventCaptureListener(target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}
function addEventCaptureListenerWithPassiveFlag(target, eventType, listener, passive) {
target.addEventListener(eventType, listener, {
capture: true,
passive: passive
});
return listener;
}
所以,V17 版本后,合成事件和原生事件的执行顺序与冒泡/捕获模式相关,冒泡模式,原生事件早于合成事件,捕获模式,合成事件早于原生事件。