前言
为什么要写这个
- 看到过很多理论文章讲了react事件系统但是一直没看到一篇文章从源码一步一步看下去,感觉很不爽
- react已经很久没有更新了,这个时候写文章大概率不担心过几天就文章过时了嘿嘿。
看文章之前我们需要先知道几个前置知识
- 事件委托,就是利用事件冒泡的特性,将事件处理程序绑定到一个父元素上,以代理所有子元素上的事件-> 具体可问gpt
- react在17版本开始将事件委托给root而不是document -> 所以我们可以从createRoot看到委托全过程
- 事件分为两个阶段,分别是冒泡和捕获,react并没有真实的将事件绑定到dom,所以冒泡和捕获的特性是react模拟的。
后续可能会大量粘贴源码,会尽量给到注释和解释。请耐心阅读
从createRoot开始
先从createRoot开始看代码,我们删除一些没用的error以及warning提示代码如下,除了一些参数设置,就干了两件事情 创建fiberRoot和 listenToAllSupportedEvents,fiber相关内容暂时不关心我们去看listenToAllSupportedEvents
JavaScript
function createRoot(container, options) {
var isStrictMode = false;
var concurrentUpdatesByDefaultOverride = false;
var identifierPrefix = '';
var onRecoverableError = defaultOnRecoverableError;
if (options !== null && options !== undefined) {
if (options.unstable_strictMode === true) {
isStrictMode = true;
}
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
if (options.transitionCallbacks !== undefined) {
transitionCallbacks = options.transitionCallbacks;
}
}
// createContainer
var root = createContainer(container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
// 其实就是给fiber设置了某个属性
markContainerAsRoot(root.current, container);
var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
下面是listenToAllSupportedEvents 代码,注意了这里给root绑定了两个事件,分别用于委托捕获的和冒泡的事件。 这里这里有个变量叫做allNativeEvents 看样子是个全局变量,我们需要找到它在哪里
JavaScript
function listenToAllSupportedEvents(rootContainerElement) {
// 开始注册监听事件!important 这里是react事件系统
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
// 注意这里的allNativeEvents是在哪里设置的
// 在registerDirectEvent方法中设置
allNativeEvents.forEach(function (domEventName) {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
// 除了nonDelegatedEvents 其余所有的event都绑定了两次一次capture一次noCapture
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
// 特殊处理,需要将selectionchange 添加到document上面
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);
}
}
}
}
看看registerDirectEvent
往上找可以找到 registerDirectEvent -> registerTwoPhaseEvent -> registerEvents$2 & registerEvents$1 & registerEvents$3 & registerEvents 。这几个函数都是立即执行的,作用是将react事件名称和原生事件名称做一一对应,比如 原生click事件 对应react的onClick事件,react的onClickCapture对应的 click事件的捕获状态。如下代码所示
JavaScript
// 原生名称和react事件名称之间的映射Map
var topLevelEventsToReactNames = new Map();
// 用于记录已经注册过的react事件名称和原生事件名称 比如 onClick -> click onClickCapture -> click
var registrationNameDependencies = {};
// 用于记录已经注册的react事件名称及其lowcase对关系
var possibleRegistrationNames = {}
// 因为每一个事件都是有两种捕获方式
// 冒泡和捕获,默认为冒泡,但是也需要提供捕获事件功能,所以这里注册事件的时候有一个Capture,
// 那么就是在react中,默认的onClick是冒泡的,而 onClickCapture则可以注册一个捕获的事件
function registerTwoPhaseEvent(registrationName, dependencies) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
// 方法中简历了已注册的react事件的lowcase和名称对应关系
// 并且记录了所有有记载的原生事件
// 请注意,这里的dependencies是一个数组,因为会有onChange和onSelect这种奇葩react事件,会有一对多原生事件的情况
function registerDirectEvent(registrationName, dependencies) {
registrationNameDependencies[registrationName] = dependencies;
{
var lowerCasedName = registrationName.toLowerCase();
possibleRegistrationNames[lowerCasedName] = registrationName;
if (registrationName === 'onDoubleClick') {
possibleRegistrationNames.ondblclick = registrationName;
}
}
for (var i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
// 注册事件名称,这一步是做了事件名称映射,比如click事件,在react中名称为onclick
// 这里是一些简单事件注册 - -其实就是满足 名称从 click -> onClick 转换的一些事件
function registerSimpleEvents() {
for (var i = 0; i < simpleEventPluginEvents.length; i++) {
var eventName = simpleEventPluginEvents[i];
var domEventName = eventName.toLowerCase();
// 将原生的eventName转换为onAbort这种形式
var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
} // Special cases where event names don't match.
// 特殊处理一些eventName
registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
registerSimpleEvent('dblclick', 'onDoubleClick');
registerSimpleEvent('focusin', 'onFocus');
registerSimpleEvent('focusout', 'onBlur');
registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}
// 以下几个特殊事件是不需要注册捕获的capture并且还有一对多的特性
function registerEvents$2() {
registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}
function registerEvents$3() {
registerTwoPhaseEvent('onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']);
}
// 非常特殊的onChange和onSelect,很多个事件都会触发onChange,同时
function registerEvents$1() {
registerTwoPhaseEvent('onChange', ['change', 'click', 'focusin', 'focusout', 'input', 'keydown', 'keyup', 'selectionchange']);
}
function registerEvents() {
registerTwoPhaseEvent('onBeforeInput', ['compositionend', 'keypress', 'textInput', 'paste']);
registerTwoPhaseEvent('onCompositionEnd', ['compositionend', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
registerTwoPhaseEvent('onCompositionStart', ['compositionstart', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
registerTwoPhaseEvent('onCompositionUpdate', ['compositionupdate', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
}
registerSimpleEvents();
registerEvents$2();
registerEvents$1();
registerEvents$3();
registerEvents();
在这里能看到一些比较有意思的是,我们可以看到某些事件其实对应了多个原生事件,比如onChange和onSelect,也就是说onChange对应的8个原生事件,任意一个触发都会触发react的onChange。
回到listenToNativeEvent
我们看listenToNativeEvent的调用链,listenToNativeEvent -> addTrappedEventListener -> createEventListenerWrapperWithPriority -> addEventCaptureListenerWithPassiveFlag & addEventCaptureListener & addEventBubbleListenerWithPassiveFlag & addEventBubbleListener。
其中最后几个addEvent都没啥好看的就是往root绑定一个事件,调用了addEventListener而已。我们主要看前面几个
javascript
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
var eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
javascript
// 学习一下如何检测是否支持passive
var passiveBrowserEventsSupported = false;
try {
var options = {};
Object.defineProperty(options, 'passive', {
get: function () {
passiveBrowserEventsSupported = true;
}
});
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (e) {
passiveBrowserEventsSupported = false;
}
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
var isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// https://github.com/facebook/react/issues/19651
// 这里对于passive做了处理,浏览器默认在document上面的滚动监听是 passive:true的,
// 也就是说滚动不会被preventDefault() 无法阻止。
if (domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel') {
isPassiveListener = true;
}
}
targetContainer = targetContainer;
var unsubscribeListener; // When legacyFBSupport is enabled, it's for when we
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);
}
}
}
关键函数为 createEventListenerWrapperWithPriority,这里面给不同类型的事件设置了不同的优先级,优先级是调度系统相关,我们先不管,我们只需要知道,最终我们给root 所有的原生dom事件都绑定了一个事件,事件名称叫dispatchEvent。
ini
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
var eventPriority = getEventPriority(domEventName);
var listenerWrapper;
switch (eventPriority) {
// dispatchDiscreteEvent 和 dispatchContinuousEvent最终还是调用了dispatchEvent只不过设置了不同的优先级而已,不需要看这两个函数。
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
dispatchEvent在干啥
我们继续看 dispatchEvent调用链 dispatchEvent -> dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay -> dispatchEventForPluginEventSystem -> dispatchEventsForPlugins。其中dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay有一段判断系统阻塞逻辑,dispatchEventForPluginEventSystem有一段逻辑用于处理存在createPortal情况下有多个root的情况,我们不需要管我们直接看dispatchEventsForPlugins
scss
function getEventTarget(nativeEvent) {
// 获取target dom的方法
var target = nativeEvent.target || nativeEvent.srcElement || window; // Normalize SVG <use> element events #4963
if (target.correspondingUseElement) {
target = target.correspondingUseElement;
} // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
// @see http://www.quirksmode.org/js/events_properties.html
//
return target.nodeType === TEXT_NODE ? target.parentNode : target;
}
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];
extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
function extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
var shouldProcessPolyfillPlugins = (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
if (shouldProcessPolyfillPlugins) {
extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
extractEvents$1(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
extractEvents$3(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
}
}
这里我们找到了我们的核心处理逻辑 extractEvents 和 processDispatchQueue。这里我们可以在 extractEvents$4方法中直接找到一个方法 accumulateSinglePhaseListeners,用于收集所有注册的click。如下所示,这里有一个重点函数 getListener,当我们知道了目标dom元素时,我们如何拿到它注册的事件回调方法呢? 直接访问element[internalPropsKey],(直接找 __reactProps 开头的字段)每一个element都会有一个internalPropsKey用于存储element对应的组件参数
kotlin
function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
var reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
var SyntheticEventCtor = SyntheticEvent;
var reactEventType = domEventName;
switch (domEventName) {
case 'keypress':
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(nativeEvent) === 0) {
return;
}
/* falls through */
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'focusout':
reactEventType = 'blur';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'beforeblur':
case 'afterblur':
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}
/* falls through */
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup': // TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent;
break;
case 'drag':
case 'dragend':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'dragstart':
case 'drop':
SyntheticEventCtor = SyntheticDragEvent;
break;
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent;
break;
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent;
break;
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent;
break;
case 'scroll':
SyntheticEventCtor = SyntheticUIEvent;
break;
case 'wheel':
SyntheticEventCtor = SyntheticWheelEvent;
break;
case 'copy':
case 'cut':
case 'paste':
SyntheticEventCtor = SyntheticClipboardEvent;
break;
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
case 'pointerdown':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerup':
SyntheticEventCtor = SyntheticPointerEvent;
break;
}
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
{
// Some events don't bubble in the browser.
// In the past, React has always bubbled them, but this can be surprising.
// We're going to try aligning closer to the browser behavior by not bubbling
// them in React either. We'll start by not bubbling onScroll, and then expand.
var accumulateTargetOnly = !inCapturePhase && // TODO: ideally, we'd eventually add all events from
// nonDelegatedEvents list in DOMPluginEventSystem.
// Then we can remove this special list.
// This is a breaking change that can wait until React 18.
domEventName === 'scroll';
var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
if (_listeners.length > 0) {
// 这个event是用于传入回调事件的,其实就是将原生event类型加了几个字段而已
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners
});
}
}
}
function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly, nativeEvent) {
var captureName = reactName !== null ? reactName + 'Capture' : null;
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,
tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode; // createEventHandle listeners
if (reactEventName !== null) {
// 请注意这里的的getListener
var listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(createDispatchListener(instance, listener, lastHostComponent));
}
}
} // If we are only accumulating events for the target, then we don't
// continue to propagate through the React fiber tree to find other
// listeners.
if (accumulateTargetOnly) {
break;
} // If we are processing the onBeforeBlur event, then we need to take
instance = instance.return;
}
return listeners;
}
这段代码很明显,我们从目标元素一直往上遍历,遍历到root位置,收集沿途所有的onClick事件并把它push到一个数组中。然后我们生成一个用于传入onClick的参数。 最后我们看到从processDispatchQueue开始processDispatchQueue -> processDispatchQueueItemsInOrder -> executeDispatch -> invokeGuardedCallbackAndCatchFirstError -> invokeGuardedCallbackImpl -> invokeGuardedCallbackImpl
ini
function invokeGuardedCallbackProd(name, func, context, a, b, c, d, e, f) {
var funcArgs = Array.prototype.slice.call(arguments, 3);
try {
// context一直都是一个undefined
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}
var invokeGuardedCallbackImpl = invokeGuardedCallbackProd;
function invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {
hasError = false;
caughtError = null;
invokeGuardedCallbackImpl$1.apply(reporter, arguments);
}
function invokeGuardedCallbackAndCatchFirstError(name, func, context, a, b, c, d, e, f) {
invokeGuardedCallback.apply(this, arguments);
if (hasError) {
var error = clearCaughtError();
if (!hasRethrowError) {
hasRethrowError = true;
rethrowError = error;
}
}
}
结尾
到此为止,已经完全弄清楚了react的事件委托的总流程
- 给root挂上捕获和冒泡的两个事件
- 点击元素后通过元素上面的__reactProps获取当前props找到对应的事件名称,然后在当前fiber节点不断向上访问,一直遍历到root收集沿途所有的事件
- 根据事件是否冒泡决定事件数组的执行顺序。
后续会了解一下react的调度机制,和react的虚拟dom对比机制。
这里react的调度机制主要依赖于 scheduler库,先看看scheduler如何使用,然后再看看scheduler在react中有什么用。
在了解完前置知识后,同样的整个分析会从 root.render()方法开始。