大家好,今天的分享由团队的 uncle13 老师提供。
React 的事件系统在前端面试中较为常见,比如“说说 React 事件与原生事件的执行顺序”,“React 事件代理机制的原理”等等。相信今天的文章读完,你都能找到答案。
本文的干货较多,推荐大家收藏后再阅读。
事件是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)。常见的事件有click
、keydown
等。通过事件监听器我们可以分配一个处理程序给对应的信号,使得浏览器和 JS 可以进行交互。当事件发生时,浏览器会创建一个事件对象,将详细信息放入其中,并将其作为参数传递给处理程序。
React 的事件系统是基于浏览器的事件机制下完全重写的,在使用上,和 DOM 元素的很相似,有几点区别:
false
的方式阻止默认行为。你必须显式的使用preventDefault
。document
节点上,通过冒泡进行触发。原生事件(阻止冒泡)会阻止合成事件的执行,合成事件(阻止冒泡)不会阻止原生事件的执行。document
),而不是每个单独的 DOM 元素。当事件发生时,React 决定哪个组件应该处理事件,这种方式称为事件委托。它减少了 DOM 操作,提高了性能。this
。addEventListener
在 document 上添加事件监听器。对于每种类型的事件(如click
、keydown
等),React 会添加一个监听器。event.preventDefault()
来阻止默认行为,或通过event.stopPropagation()
来阻止事件冒泡。document
上注册一次。input
元素增加onChange
事件,React 其实还帮我们注册了很多事件,比如keyDown
、keyUp
、blur
等,使得我们在向文本框输入内容的是你,是可以实时的得到内容的。然而原生只注册一个onchange
的话,需要在失去焦点的时候才能触发这个事件,所以这个原生事件的缺陷 react 也帮我们弥补了。react 在给 document 注册事件的时候也是对兼容性做了处理。function listen(target, eventType, callback) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
};
} else if (target.attachEvent) {
target.attachEvent('on' + eventType, callback);
return {
remove: function remove() {
target.detachEvent('on' + eventType, callback);
}
};
}
}
在 React 中,事件注册的过程涉及将 React 组件内部定义的事件处理器(event handlers)关联到实际的 DOM 元素上。这个过程是自动完成的,开发者只需在组件中声明事件处理函数即可。以下是事件注册的简化流程:
enqueueSetState
方法来安排一个更新。document
。listenTo
函数:在 React 的内部,listenTo
函数用于在 document 上监听特定的事件。这个函数会调用trapCapturedEvent
,后者负责添加特定的事件监听器。trapCapturedEvent
函数:这个函数负责添加捕获事件监听器到指定的元素。它使用addEventListener
方法来监听原生事件,并将事件的处理委托给dispatchEvent
方法。dispatchEvent
方法:当原生事件触发时,dispatchEvent
方法会被调用。这个方法负责创建和分发合成事件(synthetic events),这些合成事件是对原生事件的跨浏览器封装。下面从源码的角度简单讲一下:
在finalizeInitialChildren
函数中,通过调用setInitialProperties
来为即将渲染的 DOM 元素设置属性。对于受控组件,会特别调用ensureListeningTo
函数以确保事件监听的正确设置。
// 确保受控组件始终监听 onChange 事件,即使没有显式的监听器。
ensureListeningTo(rootContainerElement, 'onChange');
function ensureListeningTo(rootContainerElement, registrationName) {
// 判断是否是文档或文档片段
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
// 获取文档对象
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
// 调用 listenTo 函数进行事件监听
listenTo(registrationName, doc);
}
在ensureListeningTo
函数中,首先判断 DOM 元素类型是否为DOCUMENT_NODE
,这与我们之前的描述一致。接着,listenTo
函数会调用trapCapturedEvent
方法,后者使用addEventListener
来监听一个特定的事件,该事件最终会触发dispatchEvent
方法。
export function trapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
) {
if (!element) {
return null;
}
// 根据 topLevelType 判断是否为交互式事件
const dispatch = isInteractiveTopLevelEventType(topLevelType)
? dispatchInteractiveEvent
: dispatchEvent;
// 添加捕获事件监听器
addEventCaptureListener(
element,
getRawEventName(topLevelType),
// 将 dispatch 方法绑定到对应的 topLevelType
dispatch.bind(null, topLevelType),
);
}
通过上述步骤,React 完成了事件的注册声明。在 React 中,所有事件的触发都是通过统一的dispatchEvent
方法进行派发的,而不是在注册的时候直接绑定声明回调函数。这种设计允许 React 对事件进行统一的管理和优化,同时确保了事件处理的统一性和一致性。
在 React 中,所有事件的触发都通过dispatchEvent
方法统一进行派发。以下是dispatchEvent
方法的优化版本,并添加了中文注释:
export function dispatchEvent(
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
) {
// 如果事件系统被禁用,则不进行派发
if (!_enabled) {
return;
}
// 获取原生事件的目标元素
const nativeEventTarget = getEventTarget(nativeEvent);
// 获取与目标元素相关的 React 实例
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// 如果目标实例存在,但尚未挂载,则忽略该事件
if (
targetInst !== null &&
typeof targetInst.tag === 'number' &&
!isFiberMounted(targetInst)
) {
targetInst = null;
}
// 获取事件回调的上下文信息
const bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
);
try {
// 使用批处理更新来处理事件,允许在同一个周期内进行 preventDefault
batchedUpdates(handleTopLevel, bookKeeping);
} finally {
// 释放事件回调的上下文信息
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
在dispatchEvent
方法中,调用了batchedUpdates(handleTopLevel, bookKeeping)
,handleTopLevel
会遍历找出所有的父节点,并调用runExtractedEventsInBatch
中的extractEvents
生成合成事件,最后调用runEventsInBatch
执行。
export function runExtractedEventsInBatch(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
) {
// 提取合成事件
const events = extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 以批处理方式运行事件
runEventsInBatch(events, false);
}
生成合成事件后,会调用accumulateTwoPhaseDispatches(event)
,该方法最终会调用traverseTwoPhase
。
/**
* 模拟两阶段(捕获/冒泡)事件分发的遍历过程。
*/
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
// 收集从目标实例到根节点的路径
while (inst) {
path.push(inst);
inst = getParent(inst);
}
// 按捕获阶段遍历路径
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
// 按冒泡阶段遍历路径
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
在模拟过程中,所有事件监听函数及其对应的节点都会被加入到合成事件event
的属性中。
function accumulateDirectionalDispatches(inst, phase, event) {
// 开发环境下,检查实例是否为空
if (__DEV__) {
warningWithoutStack(inst, 'Dispatching inst must not be null');
}
// 获取指定阶段的监听器
const listener = listenerAtPhase(inst, event, phase);
if (listener) {
// 累积监听器和实例到合成事件中
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
最后,runEventsInBatch
会依次执行所有的事件:
export function runEventsInBatch(events) {
if (events !== null) {
// 将事件累积到事件队列中
eventQueue = accumulateInto(eventQueue, events);
}
// 处理事件队列前,将其设置为 null,以便在处理过程中检测是否有新事件加入
const processingEventQueue = eventQueue;
eventQueue = null;
if (!processingEventQueue) {
return;
}
// 遍历并执行事件队列中的事件
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
在executeDispatchesAndReleaseTopLevel
中,会判断合成事件是否isPersistent
,不是的话会释放。
const executeDispatchesAndRelease = function(
event: ReactSyntheticEvent,
simulated: boolean,
) {
if (event) {
executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
这也就是为什么当我们在需要异步读取操作一个合成事件对象的时候,需要执行event.persist()
,不然 React 就会释放掉。
最后
还没有使用过我们刷题网站(https://fe.ecool.fun/)或者前端面试题宝典的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。
老规矩,也给我们团队的辅导服务打个广告。