深入 React 合成事件机制源码与原理

您所在的位置:网站首页 事件捕获与冒泡 深入 React 合成事件机制源码与原理

深入 React 合成事件机制源码与原理

2023-10-12 07:29| 来源: 网络整理| 查看: 265

前言

本文源码基于 React v17.0.2,React 事件的基础知识就不回顾了,主要从 React 原理切入,从事件注册到事件执行的整个链路。

合成事件是什么

React 基于浏览器事件机制实现了一套自身的机制,即浏览器原生事件的跨浏览器包装器。

为什么需要合成事件 兼容所有浏览器,更好的跨平台 方便 React 统一进行事件管理,更好地控制事件的执行链路 React 17 事件特性

v17 与 v16 相比:

v16 React 将事件委托到 document 上;v17 则委托到根节点。 去除事件池 onScroll 事件不再冒泡,以防止出现常见的混淆 React 的 onFocus 和 onBlur 事件已在底层切换为原生的 focusin 和 focusout 事件。它们更接近 React 现有行为,有时还会提供额外的信息。 捕获事件(例如 onClickCapture)现在使用的是实际浏览器中的捕获监听器。 事件注册

事件注册是在顶层自执行的,在 React 自身引入文件的时候调用的。

注册事件(React 将同种类型的事件放在一个插件中):

import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin' import * as ChangeEventPlugin from './plugins/ChangeEventPlugin' import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin' import * as SelectEventPlugin from './plugins/SelectEventPlugin' import * as SimpleEventPlugin from './plugins/SimpleEventPlugin' // 原生 DOM 事件名称与 React 事件名称映射关系。其中一个作用就是给 allNativeEvents 注入所有原生事件名,下文会用到 SimpleEventPlugin.registerEvents() EnterLeaveEventPlugin.registerEvents() ChangeEventPlugin.registerEvents() SelectEventPlugin.registerEvents() BeforeInputEventPlugin.registerEvents()

registerEvents 用于初始化原生事件(其中一个作用是为 allNativeEvents集合注入原生事件名)

事件插件 SimpleEventPlugin:处理常见的 DOM 事件 EnterLeaveEventPlugin:处理鼠标进入离开时事件 ChangeEventPlugin:处理表单元素上的 onChange 事件 SelectEventPlugin:负责处理表单元素上的 onSelect 事件 BeforeInputEventPlugin:用于处理 input、textarea 或者 contentEditable 元素上的 onBeforeInput 事件 事件绑定 源码细节

在 React 初始化渲染的时候,ReactDOM.render 会调用函数 listenToAllSupportedEvents 来绑定事件

function createRootImpl(container, tag, options) { // 在根容器上监听支持的事件 const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container listenToAllSupportedEvents(rootContainerElement) }

代码位置:packages/react-dom/src/events/DOMPluginEventSystem.js,只列出 listenToAllSupportedEvents 的核心代码:

function listenToAllSupportedEvents(rootContainerElement) { if (!rootContainerElement[listeningMarker]) { // allNativeEvents 是一个 Set 集合,保存所有浏览器原生事件名 allNativeEvents.forEach((domEventName) => { // 判断是否支持冒泡的事件,不支持的话无需事件委托到根节点 if (!nonDelegatedEvents.has(domEventName)) { // 冒泡阶段绑定事件 listenToNativeEvent(domEventName, false, rootContainerElement, null) } // 捕获阶段绑定事件 listenToNativeEvent(domEventName, true, rootContainerElement, null) }) } }

listenToAllSupportedEvents 的核心逻辑:

通过 listenToNativeEvent 来绑定浏览器事件,且绑定在 rootContainerElement 根节点。 如果是支持冒泡的事件,则捕获阶段和冒泡阶段都绑定事件;不支持冒泡的事件,则只绑定捕获阶段的事件。 allNativeEvents:是一个 Set 集合,保存了 80 个浏览器原生 DOM 事件

image.png

nonDelegatedEvents:Set 集合,保存浏览器原生事件中不会冒泡的事件,如 load,scroll,媒体事件 canplay,play 等等

image.png

接下来看看 listenToNativeEvent 的实现:

创建事件委托的回调函数 根据事件是捕获阶段还是冒泡阶段,调用不同的事件绑定函数 function listenToNativeEvent( domEventName, isCapturePhaseListener, rootContainerElement, targetElement, eventSystemFlags ) { // 绑定事件 addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener) } function addTrappedEventListener( targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport ) { // 创建事件委托的回调函数(其实是事件派发器) let listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) // 根据事件是捕获阶段还是冒泡阶段,调用不同的绑定函数 if (isCapturePhaseListener) { unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener) } else { unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener) } } // 表示在冒泡阶段触发事件处理函数 function addEventBubbleListener(target, eventType, listener) { // 第三个参数为 false(冒泡阶段) target.addEventListener(eventType, listener, false) // 需要注意这里的 listener 是事件派发器,并不是我们自己使用时写的事件回调 return listener } // 表示在捕获阶段触发事件处理函数 function addEventCaptureListener(target, eventType, listener) { // 第三个参数为 true(捕获阶段) target.addEventListener(eventType, listener, true) return listener }

再来看看上面的函数 createEventListenerWrapperWithPriority 的实现:

export function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) { // 根据事件名获取事件的优先级 const eventPriority = getEventPriorityForPluginSystem(domEventName) let listenerWrapper // 根据事件优先级返回对应的事件监听函数 switch (eventPriority) { // 离散事件 case DiscreteEvent: // 优先级最高 listenerWrapper = dispatchDiscreteEvent break // 用户交互阻塞渲染的事件 case UserBlockingEvent: // 优先级适中 listenerWrapper = dispatchUserBlockingUpdate break // 其它事件 case ContinuousEvent: // 优先级最低 default: listenerWrapper = dispatchEvent break } // 返回事件回调函数 listener return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer) }

由上述代码可以看出,不同的 DOM 事件调用 getEventPriorityForPluginSystem 会返回不同的优先级,优先级包括:

DiscreteEvent:离散事件。如 click、keydown、focusin 等,这些事件的触发不是连续的,可以快速响应,优先级最高 UserBlockingEvent:用户交互阻塞渲染的事件。如 drag、scroll 等,优先级适中 ContinuousEvent 与 default:连续事件和默认事件。连续事件如 playing、load 等,优先级最低

而前两个对应的 dispatchDiscreteEvent 和 dispatchUserBlockingUpdate 其实都是对 dispatchEvent 的封装,所以下文我们重点看 dispatchEvent 函数就行了。

listenToNativeEvent

看完上面的代码,让我们来抽离出最核心的代码,把函数调用代码去掉抽离整合如下:

function listenToNativeEvent(domEventName, isCapturePhaseListener, target) { const listener = dispatchEvent.bind(null, domEventName, eventSystemFlags, targetContainer) if (isCapturePhaseListener) { target.addEventListener(eventType, listener, true) } else { target.addEventListener(eventType, listener, false) } } 核心流程 React 初始化时,会在根节点上绑定原生事件 支持冒泡的事件,React 会同时绑定捕获阶段和冒泡阶段的事件;不支持冒泡的事件,React 则只绑定捕获阶段的事件 React 将事件分为三种优先级类型,在绑定事件处理函数时会使用不同的回调函数,但底层都是调用 dispatchEvent 函数

我们也可以知道,事件在根节点中代理后是一直在触发的,只是没有绑定对应的回调函数。

问:React 事件都是在顶层进行代理派发执行的,对不支持冒泡的事件,React 如何触发

在根节点 React 只绑定了不支持冒泡事件的捕获阶段,而实际上 React 会对不支持冒泡的事件(除了 scroll)进行特殊处理,这个过程发生在 DOM 实例的创建阶段(completeWork),React 会直接把事件绑定在具体的元素上

function setInitialProperties(domElement, tag, rawProps, rootContainerElement) { switch (tag) { case 'img': case 'image': case 'link': // We listen to these events in case to ensure emulated bubble // listeners still fire for error and load events. listenToNonDelegatedEvent('error', domElement) // 传入事件名与具体的 DOM 元素 listenToNonDelegatedEvent('load', domElement) props = rawProps break // ... } } // 绑定非代理事件 function listenToNonDelegatedEvent(domEventName, targetElement) { const isCapturePhaseListener = false // 非捕获阶段(冒泡阶段) useCapture: false const listenerSet = getEventListenerSet(targetElement) const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener) if (!listenerSet.has(listenerSetKey)) { // 绑定事件 addTrappedEventListener( targetElement, // 绑定到具体 DOM 元素 domEventName, IS_NON_DELEGATED, // 非代理事件 isCapturePhaseListener ) listenerSet.add(listenerSetKey) } } 事件触发 dispatchEvent 调用链路

dispatchEvent 函数执行时调用的关键函数如下:

1. `attemptToDispatchEvent` 2. `dispatchEventForPluginEventSystem` 3. `dispatchEventsForPlugins` 4. `extractEvents`(`SimpleEventPlugin.extractEvents`...) 5. `processDispatchQueue`

当 DOM 事件触发之后, 会进入到 dispatchEvent这个回调函数,里面会调用 attemptToDispatchEvent 这个方法,作用是尝试调度事件

function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) { // ... // 尝试派发事件 const blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) // 尝试派发事件成功,则 return, 下方的代码无需执行 if (blockedOn === null) { // ... return } // 派发事件 dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer) }

attemptToDispatchEvent 函数的逻辑:

获取触发事件的 DOM 元素 根据该 DOM 元素对应的 fiber 节点 通过事件插件系统,派发事件 function attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) { // 获取触发事件的 DOM 元素(即获取 nativeEvent.target 属性) const nativeEventTarget = getEventTarget(nativeEvent) // nativeEvent 是原生事件对象 // 根据该 DOM 元素对应的 fiber 节点 let targetInst = getClosestInstanceFromNode(nativeEventTarget) // ... // 通过插件系统,派发事件 dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) }

dispatchEventForPluginEventSystem 会收集 Fiber 节点上的事件,并派发事件(批量更新 batchUpdate)

function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) { // 打开批处理 batchedEventUpdates(() => dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst, targetContainer) ) } function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) { // 获取触发事件的 DOM 元素(即获取 nativeEvent.target 属性) const nativeEventTarget = getEventTarget(nativeEvent) // 初始化事件派发队列,用于储存 listener const dispatchQueue = [] // 1. 创建合成事件,并收集同类型事件添加到 dispatchQueue 中 extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer ) // 2. 根据事件派发队列执行事件派发 processDispatchQueue(dispatchQueue, eventSystemFlags) } 合成事件

extractEvents 函数会进行事件合成,遍历 Fiber 链表,把收集同类型事件加入到 dispatchQueue 队列。

function extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) { SimpleEventPlugin.extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); const shouldProcessPolyfillPlugins = (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0; if (shouldProcessPolyfillPlugins) { EnterLeaveEventPlugin.extractEvents(...) ChangeEventPlugin.extractEvents(...) SelectEventPlugin.extractEvents(...) BeforeInputEventPlugin.extractEvents(...) } }

在合成事件中,会根据 domEventName 来决定使用哪种类型的合成事件。

SimpleEventPlugin 提供了 React 事件系统的基本功能,以 SimpleEventPlugin.extractEvents 为例,看看这个函数内部的关键代码:

根据原生事件名,得到对应的 React 事件名,并根据不同原生事件名取不同的合成事件构造函数(如 SyntheticEventCtor) 从当前 Fiber 节点出发,分别在捕获阶段和冒泡阶段收集节点上所有监听该事件的 listener 往事件派发队列 dispatchQueue 添加事件(在此注入合成事件实例和收集的同类型事件数组) // 根据原生事件名得到 React 事件名 const reactName = topLevelEventsToReactNames.get(domEventName) // 合成事件实例 let SyntheticEventCtor = SyntheticEvent // switch (domEventName) // 不同事件名 // SyntheticEventCtor = xxx // 赋予相应的合成事件构造函数 // 收集节点上所有监听该事件的 listener,向上遍历直到根节点 const listeners = accumulateSinglePhaseListeners( targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly ) if (listeners.length > 0) { const event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget) // 往事件派发队列添加事件(注入合成事件实例与同类型事件监听数组) dispatchQueue.push({ event, listeners }) }

问:如何收集 DOM 节点上的事件?

答:

{ console.log('click) }}>

React 会给该 div Fiber 节点的 props 上添加 onClick 属性;Fiber 节点中有一个属性 return,通过它可以找到它对应的父节点 Fiber ,这样就可以依次向上遍历父节点的 props 属性有无 onClick 属性,有则添加收集起来,所谓收集也就是从 props 中取出来。

抽离整理 SyntheticEventCtor的关键实现:

export const SyntheticEvent = createSyntheticEvent(EventInterface) // 不同事件类型不同的 Interface function createSyntheticEvent(Interface) { function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) { this.isPropagationStopped = functionThatReturnsFalse // ... // 在合成事件构造函数的原型上添加 Object.assign(SyntheticBaseEvent.prototype, { // 阻止默认事件 preventDefault: function () { if (event.preventDefault) { event.preventDefault() } this.isDefaultPrevented = functionThatReturnsTrue }, // 阻止冒泡 stopPropagation: function () { if (event.stopPropagation) { event.stopPropagation() } this.isPropagationStopped = functionThatReturnsTrue }, // 17 版本去除了事件池,persist 和 isPersistent 都没有用了,但为了向下兼容保留 persist: function () { // Modern event system doesn't use pooling. }, isPersistent: functionThatReturnsTrue, }) } return SyntheticBaseEvent } function functionThatReturnsTrue() { return true } 这里就可以清晰知道,我们平时在 React 事件使用的 e.preventDefault 和 e.stopPropagation 都是 React 重写封装的,而且是写在合成对象构造函数原型上,且同类型的事件会复用同一个合成事件实例对象。 事件派发

processDispatchQueue函数:遍历 dispatchQueue 数组 ,调用 processDispatchQueueItemsInOrder 函数派发事件

function processDispatchQueue(dispatchQueue, eventSystemFlags) { // 是否是捕获阶段,关系到后面执行的顺序 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0 // 循环收集的事件数组 for (let i = 0; i < dispatchQueue.length; i++) { const { event, listeners } = dispatchQueue[i] processDispatchQueueItemsInOrder(event, listeners, inCapturePhase) } }

processDispatchQueueItemsInOrder:

收集 dispatchListeners 时,是从当前 Fiber 节点遍历至根节点,所以可理解顺序遍历就是冒泡的顺序 根据事件阶段(冒泡或捕获)来决定是顺序还是倒序遍历合成事件中的 listeners。捕获阶段是从上往下调用 Fiber 树中绑定的回调函数,所以倒序遍历;而冒泡阶段是从下往上调用 Fiber 中的回调函数,所以是顺序遍历 最后执行 executeDispatch 真正派发了事件,在 Fiber 节点上绑定的 listener 也就被执行了。 function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) { let previousInstance if (inCapturePhase) { // 捕获阶段,倒序遍历 for (let i = dispatchListeners.length - 1; i >= 0; i--) { const { instance, currentTarget, listener } = dispatchListeners[i] // 判断当前是否已停止冒泡了,是则直接 return // 如果 e.stopPropagation() 方法被调用过,则会一直返回 true,否则默认一直返回 false if (instance !== previousInstance && event.isPropagationStopped()) { return } executeDispatch(event, listener, currentTarget) previousInstance = instance } } else { // 冒泡阶段,顺序遍历 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 } } } function executeDispatch(event, listener, currentTarget) { const type = event.type || 'unknown-event' event.currentTarget = currentTarget invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event) event.currentTarget = null // 重置 }

由上面可知,React 模拟原生事件捕获与冒泡的执行顺序,本质是靠向上搜集事件后,控制事件的遍历顺序去模拟的。

核心流程 在触发事件之前,React 会根据当前实际触发事件的 DOM 元素找到其 Fiber 节点,向上收集同类型事件添加到事件队列中。 根据事件阶段(冒泡/捕获),来决定(顺序/倒序)遍历执行事件函数。 当调用 React 阻止冒泡方法时,就是把变量 isPropagationStopped 设置为一个返回 true 的函数,后续派发事件时只要代码判断时则执行函数结果为 true 则表示阻止冒泡,就不再走下面逻辑。 React 事件原理概述

接下来读完全文,来总结一下 React 事件机制:

React 代码执行时,顶层会自动执行事件的注册,初始化事件插件。 React 首次渲染时,会在根节点上绑定所有原生事件。支持冒泡的事件,React 会同时绑定捕获阶段和冒泡阶段的事件;不支持冒泡的事件,会将事件绑定在具体 DOM 元素上。 事件触发前会从目标元素的 Fiber 节点向上收集同类型事件队列,构造合成对象,同类型的事件会复用同一个合成事件实例对象。 根据监听的事件阶段,决定顺序还是倒序遍历执行事件处理函数(模拟事件的冒泡捕获机制)。 参考文章 React 合成事件 React 源码解读之合成事件 深度分析 React 源码中的合成事件

本文正在参加「金石计划」



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3