背景
相传在很久很久以前,浏览器有好多不同的类别,比如谷歌内核,IE 内核,火狐内核...每个内核开发的浏览器,绑定事件的方式和对事件的一些处理都有一些微妙的差别,React 为了抹平差异,其内部对基础事件进行了一层封装,使得开发者更加方便快速的进行开发。
大纲
在 React 中对事件的处理分为事件注册和事件派发。
事件注册:
事件派发
listenToAllSupportedEvents(事件注册)
function listenToAllSupportedEvents(rootContainerElement) {
// ...
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);
}
});
// ...
}
listenToAllSupportedEvents函数接收一个参数,这个参数是当前绑定 React 的根节点,比如
。同时allNativeEvents是一个事件名称的集合,通过遍历allNativeEvents调用两次listenToNativeEvent,分别对应着事件捕获和事件冒泡。其核心方法是listenToNativeEvent,下面来看一下这个函数
listenToNativeEvent
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
// ...
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener
);
// ...
}
这个函数也很简单,主要是调用了addTrappedEventListener来对事件进行了处理,我们来一看究竟。
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
);
}
}
}
- 通过createEventListenerWrapperWithPriority创建了一个监听器
- 通过 addEventXXX进行事件的注册。
分别来看一下这两块
createEventListenerWrapperWithPriority
function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
// ...
var eventPriority = getEventPriority(domEventName);
var listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer
);
}
- 获取事件优先级
- 通过不同的优先级返回不同的触发事件的函数:dispatchXXX
- 通过bind 绑定预制参数。
获取事件优先级比较简单
function getEventPriority(domEventName) {
switch (domEventName) {
// Used by SimpleEventPlugin:
case "cancel":
case "click":
case "close":
case "contextmenu":
case "copy":
case "cut":
case "auxclick":
case "dblclick":
case "dragend":
case "dragstart":
case "drop":
case "focusin":
case "focusout":
case "input":
case "invalid":
case "keydown":
case "keypress":
case "keyup":
case "mousedown":
case "mouseup":
case "paste":
case "pause":
case "play":
case "pointercancel":
case "pointerdown":
case "pointerup":
case "ratechange":
case "reset":
case "resize":
case "seeked":
case "submit":
case "touchcancel":
case "touchend":
case "touchstart":
case "volumechange": // Used by polyfills:
// eslint-disable-next-line no-fallthrough
case "change":
case "selectionchange":
case "textInput":
case "compositionstart":
case "compositionend":
case "compositionupdate": // Only enableCreateEventHandleAPI:
// eslint-disable-next-line no-fallthrough
case "beforeblur":
case "afterblur": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough
case "beforeinput":
case "blur":
case "fullscreenchange":
case "focus":
case "hashchange":
case "popstate":
case "select":
case "selectstart":
return DiscreteEventPriority;
case "drag":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "mousemove":
case "mouseout":
case "mouseover":
case "pointermove":
case "pointerout":
case "pointerover":
case "scroll":
case "toggle":
case "touchmove":
case "wheel": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough
case "mouseenter":
case "mouseleave":
case "pointerenter":
case "pointerleave":
return ContinuousEventPriority;
case "message": {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
var schedulerPriority = getCurrentPriorityLevel();
switch (schedulerPriority) {
case ImmediatePriority:
return DiscreteEventPriority;
case UserBlockingPriority:
return ContinuousEventPriority;
case NormalPriority:
case LowPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;
case IdlePriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}
可以看到就是根据事件名返回不同的优先级。
addEventXXXListener
function addEventCaptureListener(target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}
这里可以看到就是将事件委托到了根dom上。
总结
- allNativeEvents维护了所有的事件名,通过遍历这个数组,将所有的事件委托到根dom 上。
- 总共有两次listenToNativeEvent的调用:事件捕获、事件委托。
- 创建一个listener监听器,然后注册到根 dom。
dispatchDiscreteEvent(事件派发)
这是一个组件,当点击 onClick 时,由于所有的事件都委托到了根 dom 节点,通过根 dom 节点进行派发。派发的入口函数是dispatchXXX,就是事件注册时 switch 返回的那个函数。我们直接看dispatchEvent
function dispatchEvent(){
// ...
var blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
);
//...
}
核心方法是attemptToDispatchEvent,直接看这个函数
attemptToDispatchEvent
function attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent
) {
var nativeEventTarget = getEventTarget(nativeEvent);
var targetInst = getClosestInstanceFromNode(nativeEventTarget);
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
);
}
- 根据nativeEvent获取真实触发事件的 dom 元素。
- 获取 dom 元素对应的 Fiber 节点。
- dispatchEventForPluginEventSystem
dispatchEventForPluginEventSystem
function dispatchEventForPluginEventSystem(){
batchedUpdates(function () {
return dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst
);
});
}
主要就是通过batchedUpdates来执行匿名函数,重要的是dispatchEventsForPlugins
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);
}
这个方法就比较核心了,主要干了三件事情,我们一个一个看:
- 通过nativeEvent获取对应的真实触发事件的 dom 元素。
- extractEvents$5:主要就是根据 fiberNode 以及事件名获取开发者写好的事件回调函数。
- processDispatchQueue
extractEvents$5
function extractEvents$5(){
extractEvents$4(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
);
}
extractEvents$4
function extractEvents$4(){
// ...
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,
});
}
}
- 通过accumulateSinglePhaseListeners获取开发者注册的事件回调
- 创建一个 event 对象,然后将这次的事件任务入 dispatchQueue 的任务队列。
accumulateSinglePhaseListeners
function accumulateSinglePhaseListeners(
targetFiber,
reactName,
nativeEventType,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
) {
//...
var listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent)
);
}
// ...
}
- 通过getListener获得 FiberNode 上开发者注册的函数。
function getListener(inst, registrationName) {
var stateNode = inst.stateNode;
if (stateNode === null) {
// Work in progress (ex: onload events in incremental mode).
return null;
}
var props = getFiberCurrentPropsFromNode(stateNode);
if (props === null) {
// Work in progress.
return null;
}
var listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
if (listener && typeof listener !== "function") {
throw new Error(
"Expected `" +
registrationName +
"` listener to be a function, instead got a value of `" +
typeof listener +
"` type."
);
}
return listener;
}
2. 将函数入队列。
processDispatchQueue
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();
}
- 遍历dispatchQueue,拿出listeners。
- processDispatchQueueItemsInOrder
processDispatchQueueItemsInOrder
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;
}
}
}
- 通过inCapturePhase判断是捕获阶段还是冒泡阶段。
- 遍历dispatchListeners,executeDispatch执行
executeDispatch
function executeDispatch(event, listener, currentTarget) {
var type = event.type || "unknown-event";
event.currentTarget = currentTarget;
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
- invokeGuardedCallbackAndCatchFirstError
invokeGuardedCallbackAndCatchFirstError
这个函数内部调用了真正的回调进行执行。
fakeNode.addEventListener(evtType, callCallback, false); // Synchronously dispatch our fake event. If the user-provided function
// errors, it will trigger our global error handler.
evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);
可以看到就是监听了一个事件监听,然后直接dispatch触发。
常见用法原理
阻止事件冒泡
在进行事件处理时,会涉及阻止事件冒泡的操作,一般都是用e.stopPropagation()进行阻止。但是从上面的分析中我们知道,React 是合成事件,通过收集 JSX 上的事件注册函数,然后遍历来调用。那用的应该不是本身自带的冒泡方式。下面来分析一下如何办到的。
看一下这段代码
<div
onClick={() => {
console.log("向上冒泡");
}}
>
包裹住
<button
onClick={(e) => {
e.stopPropagation()
debugger;
}}
>
测试事件
</button>
</div>
可以看到这里有两个事件回调,正常会先触发 button 的事件回调,然后向上冒泡到 div,触发 div 的事件回调。
但是我这里通过stopPropagation阻止了事件冒泡,所以不会冒泡到 div。我们一看究竟。
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;
}
这里通过遍历收集的dispatchListeners,然后循环调用。这里分析一下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;
}
- 首先进行兼容,IE 内核和谷歌内核的取消事件冒泡方式是不同的,所以这里进行了判断,分别进行兼容。
- 将isPropagationStopped设置为 true。
- 然后在循环调用的时候,就走到了 if 里,直接 return 了,这就是阻止冒泡的原理。
总结
事件注册
- 对事件捕获和事件冒泡分别进行处理。
- 将所有事件注册到根 dom 元素上。
- 对listeners事件函数进行包装,注册到根 dom 上。
事件派发
- 触发到根 root。
- 收集注册到事件函数到数组中。
- 循环遍历数组进行执行。同时还对冒泡相关进行了包装,抹平了浏览器的差异。