- React利用事件委托机制在根DOM容器上统一监听DOM事件,再根据触发的target将事件分发到具体的组件实例。另外event参数是一个合成事件对象(SyntheticEvent), 而不是原始的DOM事件对象.
- 不是直接触发绑定的事件回调,而是先触发绑定到根DOM容器上的对应类型事件,根据event.target找到触发事件的节点,然后将合成事件的event传入事件回调中触发
- 只要有正确的event,事件和由谁来触发没有任何关系
React自定义的事件系统特点:
- 抹平浏览器之间的兼容性差异。
- 事件自定义,表单onChange事件,它为表单元素定义了统一的值变动事件。也可以通过React的事件插件机制来合成自定义事件
- 抽象跨平台事件机制
- 利用事件委托机制,简化了DOM事件处理逻辑,减少了内存开销. 但这也意味着,React需要自己模拟一套事件冒泡的机制。
- 区分事件优先级
从v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中.
事件注册:
<div onClick={this.handle}></div>
- 实际上onClick方法会传入fiber的props中,并不像原生事件监听。
registerDirectEvent
- 把react事件名称跟对应的dependencies关系存到registrationNameDependencies中:维护一个数据结构 {‘onclick’:[‘click’],… }
- 把所有的依赖存入allNativeEvents中
- 非委派事件,即不支持冒泡的事件,不会存入,主要是媒体事件video、scroll等会绑在自己对应的el节点上
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]);
}
}
创建fiberRoot对象时, 调用createRootImpl:
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// ... 省略无关代码
if (enableEagerRootListeners) {
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
}
// ... 省略无关代码
}
listenToAllSupportedEvents
- 遍历事件名称,调用listenToNativeEvent监听冒泡或捕获阶段的事件
var mediaEventTypes = ['abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting'];
var nonDelegatedEvents = new Set(['cancel', 'close', 'invalid', 'load', 'scroll', 'toggle'].concat(mediaEventTypes));
// ...
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (enableEagerRootListeners) {
// 1. 节流优化, 保证全局注册只被调用一次
if ((rootContainerElement: any)[listeningMarker]) {
return;
}
(rootContainerElement: any)[listeningMarker] = true;
// 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
allNativeEvents.forEach(domEventName => {
//不支持委托的事件,即不支持冒泡的事件不会进入
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false, // 冒泡阶段监听
((rootContainerElement: any): Element),
null,
);
}
listenToNativeEvent(
domEventName,
true, // 捕获阶段监听
((rootContainerElement: any): Element),
null,
);
});
}
}
listenToNativeEvent
- 通过addTrappedEventListener监听事件
// ...
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
//获取存储事件的集合
const listenerSet = getEventListenerSet(target);
//获取遍历的事件的key
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
// 利用set数据结构, 保证相同的事件类型只会被注册一次.
if (!listenerSet.has(listenerSetKey)) {
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// 注册事件监听
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
listenerSet.add(listenerSetKey);
}
}
addTrappedEventListener
- 根据事件名称,构造不同类型事件的listener回调
- addEventCaptureListener、addEventBubbleListener往根容器上注册事件
- 后续节点上触发的监听会是对应的listener
// ...
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 1. 构造不同类型事件的listener回调
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let unsubscribeListener;
// 2. 注册事件监听
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
// 注册原生事件 冒泡
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}
// 注册原生事件 捕获
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
createEventListenerWrapperWithPriority
- 根据不同类型事件的优先级,构造listener事件回调
-
DiscreteEvent 离散事件. 例如blur、focus、 click、 submit、 touchStart. 这些事件都是离散触发的
-
UserBlockingEvent 用户阻塞事件. 例如touchMove、mouseMove、scroll、drag、dragOver等等。这些事件会’阻塞’用户的交互。
-
ContinuousEvent 可连续事件。例如load、error、loadStart、abort、animationEnd. 这个优先级最高,也就是说它们应该是立即同步执行的,这就是Continuous的意义,即可连续的执行,不被打断.
-
对应scheduler中的优先级
- Immediate - 这个优先级的任务会同步执行, 或者说要马上执行且不能中断,对应ContinuousEvent
- UserBlocking(250ms timeout) 这些任务一般是用户交互的结果, 需要即时得到反馈 .对应UserBlockingEvent(先执行)、DiscreteEvent(后执行)
- Normal (5s timeout) 应对哪些不需要立即感受到的任务,例如网络请求
- Low (10s timeout) 这些任务可以放后,但是最终应该得到执行. 例如分析通知
- Idle (no timeout) 一些没有必要做的任务 (e.g. 比如隐藏的内容).
-
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 1. 根据优先级设置 listenerWrapper
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
// 2. 返回 listenerWrapper
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
dispatchEvent
// ...省略无关代码
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (!_enabled) {
return;
}
const blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
attemptToDispatchEvent
- 把原生事件和fiber树关联起来.
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// ...
// 1. 定位原生DOM节点
const nativeEventTarget = getEventTarget(nativeEvent);
// 2. 获取与DOM节点对应的fiber节点
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// 3. 通过插件系统, 派发事件
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
return null;
}
- SimpleEventPlugin - 简单事件, 处理一些比较通用的事件类型,例如click、input、keyDown、mouseOver、mouseOut、pointerOver、pointerOut
- EnterLeaveEventPlugin - mouseEnter/mouseLeave和pointerEnter/pointerLeave这两类事件比较特殊, 和*over/*out事件相比, 它们不支持事件冒泡, enter会给所有进入的元素发送事件, 行为有点类似于:hover; 而over在进入元素后,还会冒泡通知其上级. react通过over、out事件来模拟的enter、leave事件
- ChangeEventPlugin - change事件是React的一个自定义事件,旨在规范化表单元素的变动事件。它支持这些表单元素: input, textarea, select
- SelectEventPlugin - 和change事件一样,React为表单元素规范化了select(选择范围变动)事件,适用于input、textarea、contentEditable元素
- BeforeInputEventPlugin - beforeinput事件以及composition事件处理
在之前的版本中,为了避免频繁创建和释放事件对象导致性能损耗(对象创建和垃圾回收),React使用一个事件池来负责管理事件对象,使用完的事件对象会放回池中,以备后续的复用使用一个对象,在事件处理器同步执行完后,SyntheticEvent对象就会马上被回收,所有属性都会无效, 也就是说SyntheticEvent不能用于异步引用,它在同步执行完事件处理器后就会被释放。在异步操作中访问SyntheticEvent事件对象,通过event.persist()方法,告诉React不要回收到对象池
收集对应 fiber 上的 注册的事件回调
SimpleEventPlugin.extractEvents
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
//合成事件event的构造方法
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
//如果是scroll事件或捕获事件,不向上收集
const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
// 1. 收集所有监听该事件的函数.
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
);
if (listeners.length > 0) {
// 2. 构造合成事件的event, 添加到派发队列
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
//存储该事件所有的合成event和事件回调
dispatchQueue.push({ event, listeners });
}
}
accumulateSinglePhaseListeners
- 收集对应事件的所有回调(冒泡)
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
): Array<DispatchListener> {
//定义事件名称
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
//存储该事件所有回调的数组
const listeners: Array<DispatchListener> = [];
//触发事件的fiber节点
let instance = targetFiber;
let lastHostComponent = null;
// 从targetFiber开始, 向上遍历, 直到 root 为止
while (instance !== null) {
const { stateNode, tag } = instance;
// 当节点类型是HostComponent时(如: div, span, button等类型)
if (tag === HostComponent && stateNode !== null) {
//获取fiber节点对应的实例
lastHostComponent = stateNode;
if (reactEventName !== null) {
// 获取标准的监听函数 (如onClick , onClickCapture等)
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
// 如果只收集目标节点, 则不用向上遍历, 直接退出(捕获事件和scroll)
if (accumulateTargetOnly) {
break;
}
instance = instance.return;
}
return listeners;
}
合成事件的event
SyntheticEvent
- 原生event事件的包装器,抹平不同浏览器 api 的差异
type EventInterfaceType = {
[propName: string]: 0 | ((event: {[propName: string]: mixed}) => mixed),
};
function functionThatReturnsTrue() {
return true;
}
function functionThatReturnsFalse() {
return false;
}
function createSyntheticEvent(Interface: EventInterfaceType) {
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber,
nativeEvent: {[propName: string]: mixed},
nativeEventTarget: null | EventTarget,
) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
//遍历设置event上的属性
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
this[propName] = nativeEvent[propName];
}
}
const defaultPrevented =
nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
//阻止冒泡的标识
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
//定义原型方法
Object.assign(SyntheticBaseEvent.prototype, {
preventDefault: function() {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}
//兼容不同平台的preventDefault
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() {
const event = this.nativeEvent;
if (!event) {
return;
}
//兼容不同平台的stopPropagation
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,
});
return SyntheticBaseEvent;
}
执行派发事件
- 遍历dispatchListeners数组, 执行executeDispatch派发事件, 在fiber节点上绑定的回调函数被执行.
processDispatchQueue
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
//遍历之前存储的dispatchQueue
for (let i = 0; i < dispatchQueue.length; i++) {
const { event, listeners } = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
// ...
}
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
if (inCapturePhase) {
// 1. capture事件: 倒序遍历listeners,因为是向上收集的事件,所以捕获事件需要倒叙触发
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
//如果声明了阻止冒泡
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
// 2. bubble事件: 顺序遍历listeners,因为是向上收集的事件,所以冒泡事件需要顺序触发
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}