blog
blog copied to clipboard
React合成事件源码学习
前言
本次阅读的React源码是v17.0.1,研究一下React的事件机制
疑问
下面是笔者对合成事件比较好奇的地方。
- React在哪个阶段将事件委托根节点?
- React的冒泡和捕获机制是如何模拟的?
- React事件对象是怎么复用的?
示例demo
class About extends Component {
constructor () {
super()
}
handleChildClick (e) {
console.log('child click', e)
}
handleParentClick (e) {
console.log('parent click', e)
}
render() {
return (
<div onClick={(e) => this.handleParentClick(e)}>
<button onClick={(e) => this.handleChildClick(e)}>click</button>
</div>
);
}
}
React在哪个阶段将事件委托根节点?
执行ReactDom.render的时候,会创建根节点,同时调用listenToAllSupportedEvents
,在根节点注册常用的原生事件。根结点的click
事件就是在这个时候注册的,会在根节点绑定click的冒泡事件和捕获事件,两个事件绑定的方法都是dispatchDiscreteEvent
。
注意:在 React 17 中,React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中,因为页面中可能会有多个React版本,将事件委托在document无法实现嵌套树结构的事件机制。
function listenToAllSupportedEvents(rootContainerElement) {
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach(function (domEventName) {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
var ownerDocument = rootContainerElement.nodeType === DOCUMENT_NODE ? rootContainerElement : rootContainerElement.ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!ownerDocument[listeningMarker]) {
ownerDocument[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
如果是onScroll
等不常用的事件,会在执行completeWork
方法初始化dom节点的时候将事件委托到根节点。
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
// 更新的时候
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
if (!newProps) {
if (!(workInProgress.stateNode !== null)) {
{
throw Error( "We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue." );
}
} // This can happen when we abort work.
return null;
}
var currentHostContext = getHostContext(); /
var _wasHydrated = popHydrationState(workInProgress);
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// 执行finalizeInitialChildren方法完成事件委托
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef$1(workInProgress);
}
}
return null;
}
}
}
在finalizeInitialChildren
放内部会调用setInitialProperties
方法,执行listenToNonDelegatedEvent
完成事件委托。
function listenToNonDelegatedEvent(domEventName, targetElement) {
var isCapturePhaseListener = false;
var listenerSet = getEventListenerSet(targetElement);
var listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
if (!listenerSet.has(listenerSetKey)) {
addTrappedEventListener(targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener);
listenerSet.add(listenerSetKey);
}
}
冒泡和捕获机制
react绑定的事件如果想区分是在冒泡还是捕获阶段执行,通过事件函数后缀Capture区别。比如onClick在冒泡阶段执行, onClickCapture发生在捕获阶段。
在示例demo中,点击按钮首先会触发根节点click的捕获事件,然后触发根节点的冒泡事件。因为demo中按钮绑定的是冒泡事件,所以下面我们主要看一下冒泡事件的模拟流程。
根节点冒泡事件绑定的方法dispatchDiscreteEvent
。然后通过nativeEvent.target
或者nativeEvent.srcElement
找到事件触发的位置为button,并找到对应的fiber节点。
在dispatchEventForPluginEventSystem
方法中,从事件触发的fiber节点向上查找匹配的根节点。
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var ancestorInst = targetInst;
if ((eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 && (eventSystemFlags & IS_NON_DELEGATED) === 0) {
var targetContainerNode = targetContainer; // If we are using the legacy FB support flag, we
if (targetInst !== null) {
// The below logic attempts to work out if we need to change
// the target fiber to a different ancestor. We had similar logic
// in the legacy event system, except the big difference between
// systems is that the modern event system now has an event listener
// attached to each React Roo and React Portal Root. Together,
// the DOM nodes representing these roots are the "rootContainer".
// To figure out which ancestor instance we should use, we traverse
// up the fiber tree from the target instance and attempt to find
// root boundaries that match that of our current "rootContainer".
// If we find that "rootContainer", we find the parent fiber
// sub-tree for that root and make that our instance.
var node = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
var nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
var container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
// The target is a portal, but it's not the rootContainer we're looking for.
// Normally portals handle their own events all the way down to the root.
// So we should be able to stop now. However, we don't know if this portal
// was part of *our* root.
var grandNode = node.return;
while (grandNode !== null) {
var grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
var grandContainer = grandNode.stateNode.containerInfo;
if (isMatchingRootContainer(grandContainer, targetContainerNode)) {
// This is the rootContainer we're looking for and we found it as
// a parent of the Portal. That means we can ignore it because the
// Portal will bubble through to us.
return;
}
}
grandNode = grandNode.return;
}
} // Now we need to find it's corresponding host fiber in the other
// tree. To do this we can use getClosestInstanceFromNode, but we
// need to validate that the fiber is a host instance, otherwise
// we need to traverse up through the DOM till we find the correct
// node that is from the other tree.
while (container !== null) {
var parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
var parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
batchedEventUpdates(function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
}
执行dispatchEventsForPlugins
方法, 开始模拟事件冒泡。
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];
// 找到所有绑定了相应事件的节点,生成合成事件实例,并放入dispatchQueue队列中
extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
// 执行dispatchQueue队列中的绑定的方法
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
在extractEvents$5
方法中通过从触发事件的fiber节点,向上查找所有绑定了click冒泡事件的fiber节点,并生成合成事件放入队列中。_listeners
中会依次放入button和div两个事件执行对象。
var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
if (_listeners.length > 0) {
// Intentionally create event lazily.
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners
});
}
最后在processDispatchQueue
执行dispatchQueue队列中的事件绑定方法。
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
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();
}
React事件对象是如何复用的?
很遗憾,React17已经去除了事件池。因为复用了事件对象,在某些情况会导致崩溃。
function handleChange(e) {
setData(data => ({
...data,
// This crashes in React 16 and earlier:
text: e.target.value
}));
}
这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用 e.persist() 才能正确的使用该事件,或者正确读取需要的属性。
合成事件构造函数。不再使用事件池,persist
方法因此也变成了一个空函数。
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
for (var _propName in Interface) {
if (!Interface.hasOwnProperty(_propName)) {
continue;
}
var normalize = Interface[_propName];
if (normalize) {
this[_propName] = normalize(nativeEvent);
} else {
this[_propName] = nativeEvent[_propName];
}
}
var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
_assign(SyntheticBaseEvent.prototype, {
preventDefault: function () {
this.defaultPrevented = true;
var event = this.nativeEvent;
if (!event) {
return;
}
if (event.preventDefault) {
event.preventDefault(); // $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
stopPropagation: function () {
var event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
/**
* We release all dispatched `SyntheticEvent`s after each event loop, adding
* them back into the pool. This allows a way to hold onto a reference that
* won't be added back into the pool.
*/
persist: function () {// Modern event system doesn't use pooling.
},
/**
* Checks if this event should be released back into the pool.
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: functionThatReturnsTrue
});