前言
先看官网对17版本的介绍:
React v17 中,React 不会再将事件处理添加到 document
上,而是将事件处理添加到渲染 React 树的根 DOM 容器中:
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
在 React 16 及之前版本中,React 会对大多数事件进行 document.addEventListener()
操作。React v17 开始会通过调用 rootNode.addEventListener()
来代替。
官方还提供了一幅示意图:
来看下react17输出的页面事件绑定结果是什么样子的: 再看下html: 可以看到事件都在root节点监听了而且监听了全部的事件。下面我们就开始剖析17的事件机制。
事件机制
可以简单概括为事件绑定、处理合成事件、收集事件响应链路、触发事件回调函数。
事件绑定
函数调用关系:
graph TD
listenToAllSupportedEvents --> listenToNativeEvent --> addTrappedEventListener
下面我们再具体看一下每个函数主要做了什么:
listenToAllSupportedEvents
function listenToAllSupportedEvents(rootContainerElement) {
// ...
// allNativeEvents是所有的native事件
allNativeEvents.forEach(function (domEventName) {
// nonDelegatedEvents中的事件都是不能冒泡的元素,如果该事件不能冒泡,就只绑定捕获阶段,否则两个阶段都绑定
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement, null);
}
listenToNativeEvent(domEventName, true, rootContainerElement, null);
});
}
listenToNativeEvent
function listenToNativeEvent(domEventName, isCapturePhaseListener, rootContainerElement, targetElement) {
// ...
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
// ...
}
addTrappedEventListener
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
// 根据事件名,创建优先级不同的监听器
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
// 绑定监听器
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);
}
}
}
对于不向上冒泡的事件如何处理?
非常简单,直接在绑定对应事件的dom节点上监听就完事了,涉及到的代码如下:
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
// ...
switch (tag) {
case 'dialog':
listenToNonDelegatedEvent('cancel', domElement);
listenToNonDelegatedEvent('close', domElement);
break;
case 'iframe':
case 'object':
case 'embed':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the load event.
listenToNonDelegatedEvent('load', domElement);
break;
// ...
}
// ...
} // Calculate the diff between the two objects.
其中listenToNonDelegatedEvent
方法会去调用addTrappedEventListener
方法完成事件绑定。最后效果如下:
处理合成事件
调用dispatchEventsForPlugins方法,对native事件进行合成,网上相关的文章很多,这里就不多说了。涉及到的核心方法如下:
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);
}
dispatchQueue
是一个数组,每一项包含了一个合成事件及其该合成事件对应的回调函数(listeners),extractEvents
负责合成事件并调用相关的方法来收集事件响应链路,processDispatchQueue
负责执行事件的回调函数
收集事件响应链路
该阶段会从事件发生源对应的dom节点对应的fiber节点开始,逐一遍历父fiber节点直到根fiber节点,寻找fiber节点props中对应事件函数,比如触发了onClick事件,就在fiber节点的props中找onClick,如果有对应的事件名及其函数,那么就按照顺序收集起来。核心方法:accumulateSinglePhaseListeners
、accumulateTwoPhaseListeners
。下面以accumulateSinglePhaseListeners
为例
function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly) {
var captureName = reactName !== null ? reactName + 'Capture' : null;
// 根据一开始设定的事件监听阶段,设置不同的事件名,以onClick为例,reactName是onClick captureName是onClickCapture
var reactEventName = inCapturePhase ? captureName : reactName;
var listeners = [];
var instance = targetFiber;
var lastHostComponent = null; // Accumulate all instances and listeners via the target -> root path.
while (instance !== null) {
var _instance2 = instance,
stateNode = _instance2.stateNode, // stateNode是fiber节点对应的dom节点
tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)
// react只处理在dom节点上绑定的事件
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode; // createEventHandle listeners
if (reactEventName !== null) {
var listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(createDispatchListener(instance, listener, lastHostComponent));
}
}
}
// ...
// 向上找
instance = instance.return;
}
return listeners;
}
function getListener(inst, registrationName) {
var stateNode = inst.stateNode;
// ...
// 获得dom节点对应的fiber节点上的props
var props = getFiberCurrentPropsFromNode(stateNode);
// 获取同名的事件回调函数
var listener = props[registrationName];
// ...
return listener;
}
以下面的组件为例:
function App() {
return (
<div className="App">
<header onClick={() => { console.log('header') }} className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<code onClick={() => { console.log('code') }}>src/App.js</code>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
最后收集到的listeners如下: dipatchQueue如下:
触发事件回调函数
在收集事件响应链路阶段,已经收集到了含有事件监听器的一条链路,按照收集顺序执行即从触发节点到根节点,就是模拟了冒泡,反过来就是捕获。核心方法processDispatchQueue
, processDispatchQueueItemsInOrder
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 遍历dispatchQueue并调用processDispatchQueueItemsInOrder
for (var i = 0; i < dispatchQueue.length; i++) {
var _dispatchQueue$i = dispatchQueue[i],
event = _dispatchQueue$i.event,
listeners = _dispatchQueue$i.listeners;
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, 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;
}
}
}
到此,我们就基本分析完了react17的事件机制,下面做一下总结。
总结
- react17会在rootContainer上监听所有事件的冒泡和捕获阶段,不能冒泡的事件会直接在对应的dom节点上直接绑定
- dom节点上不会绑定事件监听器(除了几种特殊情况)
- 代码element中的事件回调函数保存在fiber节点的props中
- 监听到事件后,事件会在rootContainer上处理捕获和冒泡阶段(不能冒泡的事件在对应的dom节点上处理),经过函数调用,最后会调用
dispatchEventsForPlugins
将native事件处理成合成事件,并会从触发节点的fiber节点开始向上搜寻监听相同事件的fiber节点(不能冒泡的事件不会向上搜索),将props中的回调函数放到一个数组里等待执行 - 最后根据是冒泡事件还是捕获事件,决定执行顺序 有描述错误的地方希望大佬看到可以指出来,谢谢~