当咱们在组件上设置事件处理器时,React并不会在该DOM元素上直接绑定事件处理器. React内部自定义了一套事件系统,在这个系统上统一进行事件订阅和分发.react
具体来说,React利用事件委托机制在Document上统一监听DOM事件,再根据触发的target将事件分发到具体的组件实例。另外上面e是一个合成事件对象(SyntheticEvent), 而不是原始的DOM事件对象.git
文章大纲github
截止本文写做时,React版本是16.8.6segmentfault
若是了解过Preact(笔者以前写过一篇文章解析Preact的源码),Preact裁剪了不少React的东西,其中包括事件机制,Preact是直接在DOM元素上进行事件绑定的。浏览器
在研究一个事物以前,我首先要问为何?了解它的动机,才有利于你对它有本质的认识。网络
React自定义一套事件系统的动机有如下几个:架构
1. 抹平浏览器之间的兼容性差别。 这是估计最原始的动机,React根据W3C 规范来定义这些合成事件(SyntheticEvent), 意在抹平浏览器之间的差别。异步
另外React还会试图经过其余相关事件来模拟一些低版本不兼容的事件, 这才是‘合成’的原本意思吧?。函数
2. 事件‘合成’, 即事件自定义。事件合成除了处理兼容性问题,还能够用来自定义高级事件,比较典型的是React的onChange事件,它为表单元素定义了统一的值变更事件。另外第三方也能够经过React的事件插件机制来合成自定义事件,尽管不多人这么作。post
3. 抽象跨平台事件机制。 和VirtualDOM的意义差很少,VirtualDOM抽象了跨平台的渲染方式,那么对应的SyntheticEvent目的也是想提供一个抽象的跨平台事件机制。
4. React打算作更多优化。好比利用事件委托机制,大部分事件最终绑定到了Document,而不是DOM节点自己. 这样简化了DOM事件处理逻辑,减小了内存开销. 但这也意味着,React须要本身模拟一套事件冒泡的机制。
5. React打算干预事件的分发。v16引入Fiber架构,React为了优化用户的交互体验,会干预事件的分发。不一样类型的事件有不一样的优先级,好比高优先级的事件能够中断渲染,让用户代码能够及时响应用户交互。
Ok, 后面咱们会深刻了解React的事件实现,我会尽可能不贴代码,用流程图说话。
ReactEventListener - 事件处理器. 在这里进行事件处理器的绑定。当DOM触发事件时,会从这里开始调度分发到React组件树
ReactEventEmitter - 暴露接口给React组件层用于添加事件订阅
EventPluginHub - 如其名,这是一个‘插件插槽’,负责管理和注册各类插件。在事件分发时,调用插件来生成合成事件
Plugin - React事件系统使用了插件机制来管理不一样行为的事件。这些插件会处理本身感兴趣的事件类型,并生成合成事件对象。目前ReactDOM有如下几种插件类型:
SimpleEventPlugin - 简单事件, 处理一些比较通用的事件类型,例如click、input、keyDown、mouseOver、mouseOut、pointerOver、pointerOut
EnterLeaveEventPlugin - mouseEnter/mouseLeave和pointerEnter/pointerLeave这两类事件比较特殊, 和*over/*out
事件相比, 它们不支持事件冒泡, *enter
会给全部进入的元素发送事件, 行为有点相似于:hover
; 而*over
在进入元素后,还会冒泡通知其上级. 能够经过这个实例观察enter和over的区别.
若是树层次比较深,大量的mouseenter触发可能致使性能问题。另外其不支持冒泡,没法在Document完美的监听和分发, 因此ReactDOM使用*over/*out
事件来模拟这些*enter/*leave
。
ChangeEventPlugin - change事件是React的一个自定义事件,旨在规范化表单元素的变更事件。
它支持这些表单元素: input, textarea, select
SelectEventPlugin - 和change事件同样,React为表单元素规范化了select(选择范围变更)事件,适用于input、textarea、contentEditable元素.
BeforeInputEventPlugin - beforeinput事件以及composition事件处理。
本文主要会关注SimpleEventPlugin
的实现,有兴趣的读者能够本身阅读React的源代码.
EventPropagators 按照DOM事件传播的两个阶段,遍历React组件树,并收集全部组件的事件处理器.
EventBatching 负责批量执行事件队列和事件处理器,处理事件冒泡。
SyntheticEvent 这是‘合成’事件的基类,能够对应DOM的Event对象。只不过React为了减低内存损耗和垃圾回收,使用一个对象池来构建和释放事件对象, 也就是说SyntheticEvent不能用于异步引用,它在同步执行完事件处理器后就会被释放。
SyntheticEvent也有子类,和DOM具体事件类型一一匹配:
SimpleEventPlugin将事件类型划分红了三类, 对应不一样的优先级(优先级由低到高):
可能要先了解一下React调度(Schedule)的优先级,才能理解这三种事件类型的区别。截止到本文写做时,React有5个优先级级别:
Immediate
- 这个优先级的任务会同步执行, 或者说要立刻执行且不能中断UserBlocking
(250ms timeout) 这些任务通常是用户交互的结果, 须要即时获得反馈 .Normal
(5s timeout) 应对哪些不须要当即感觉到的任务,例如网络请求Low
(10s timeout) 这些任务能够放后,可是最终应该获得执行. 例如分析通知Idle
(no timeout) 一些没有必要作的任务 (e.g. 好比隐藏的内容).目前ContinuousEvent对应的是Immediate优先级; UserBlockingEvent对应的是UserBlocking(须要手动开启); 而DiscreteEvent对应的也是UserBlocking, 只不过它在执行以前,先会执行完其余Discrete任务。
本文不会深刻React Fiber架构的细节,有兴趣的读者能够阅读文末的扩展阅读列表.
如今开始进入文章正题,React是怎么实现事件机制?主要分为两个部分: 绑定和分发.
为了不后面绕晕了,有必要先了解一下React事件机制中的插件协议。 每一个插件的结构以下:
export type EventTypes = {[key: string]: DispatchConfig};
// 插件接口
export type PluginModule<NativeEvent> = {
eventTypes: EventTypes, // 声明插件支持的事件类型
extractEvents: ( // 对事件进行处理,并返回合成事件对象
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: NativeEvent,
nativeEventTarget: EventTarget,
) => ?ReactSyntheticEvent,
tapMoveThreshold?: number,
};
复制代码
eventTypes声明该插件负责的事件类型, 它经过DispatchConfig
来描述:
export type DispatchConfig = {
dependencies: Array<TopLevelType>, // 依赖的原生事件,表示关联这些事件的触发. ‘简单事件’通常只有一个,复琐事件如onChange会监听多个, 以下图👇
phasedRegistrationNames?: { // 两阶段props事件注册名称, React会根据这些名称在组件实例中查找对应的props事件处理器
bubbled: string, // 冒泡阶段, 如onClick
captured: string, // 捕获阶段,如onClickCapture
},
registrationName?: string // props事件注册名称, 好比onMouseEnter这些不支持冒泡的事件类型,只会定义 registrationName,不会定义phasedRegistrationNames
eventPriority: EventPriority, // 事件的优先级,上文已经介绍过了
};
复制代码
看一下实例:
上面列举了三个典型的EventPlugin:
SimpleEventPlugin - 简单事件最好理解,它们的行为都比较通用,没有什么Trick, 例如不支持事件冒泡、不支持在Document上绑定等等. 和原生DOM事件是一一对应的关系,比较好处理.
EnterLeaveEventPlugin - 从上图能够看出来,mouseEnter
和mouseLeave
依赖的是mouseout
和mouseover
事件。也就是说*Enter/*Leave
事件在React中是经过*Over/*Out
事件来模拟的。这样作的好处是能够在document上面进行委托监听,还有避免*Enter/*Leave
一些奇怪而不实用的行为。
ChangeEventPlugin - onChange是React的一个自定义事件,能够看出它依赖了多种原生DOM事件类型来模拟onChange事件.
另外每一个插件还会定义extractEvents
方法,这个方法接受事件名称、原生DOM事件对象、事件触发的DOM元素以及React组件实例, 返回一个合成事件对象,若是返回空则表示不做处理. 关于extractEvents的细节会在下一节阐述.
在ReactDOM启动时就会向EventPluginHub
注册这些插件:
EventPluginHubInjection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
复制代码
Ok, 回到正题,事件是怎么绑定的呢? 打个断点看一下调用栈:
前面调用栈关于React树如何更新和渲染就不在本文的范围内了,经过调用栈能够看出React在props初始化和更新时会进行事件绑定。这里先看一下流程图,忽略杂乱的跳转:
媒体类型
,媒体类型的事件是没法在Document监听的,因此会直接在元素上进行绑定onMouseEnter
依赖mouseover/mouseout
; 第二个是ReactBrowserEventEmitter维护的'已订阅事件表'。事件处理器只需在Document订阅一次,因此相比在每一个元素上订阅事件会节省不少资源.代码大概以下:
export function listenTo( registrationName: string, // 注册名称,如onClick mountAt: Document | Element | Node, // 组件树容器,通常是Document ): void {
const listeningSet = getListeningSetForElement(mountAt); // 已订阅事件表
const dependencies = registrationNameDependencies[registrationName]; // 事件依赖
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listeningSet.has(dependency)) { // 未订阅
switch (dependency) {
// ... 特殊的事件监听处理
default:
const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt); // 设置事件处理器
}
break;
}
listeningSet.add(dependency); // 更新已订阅表
}
}
}
复制代码
function trapEventForPluginEventSystem( element: Document | Element | Node, // 绑定到元素,通常是Document topLevelType: DOMTopLevelEventType, // 事件名称 capture: boolean, ): void {
let listener;
switch (getEventPriority(topLevelType)) {
// 不一样优先级的事件类型,有不一样的事件处理器进行分发, 下文会详细介绍
case DiscreteEvent: // ⚛️离散事件
listener = dispatchDiscreteEvent.bind(
null,
topLevelType,
PLUGIN_EVENT_SYSTEM,
);
break;
case UserBlockingEvent: // ⚛️用户阻塞事件
listener = dispatchUserBlockingUpdate.bind(
null,
topLevelType,
PLUGIN_EVENT_SYSTEM,
);
break;
case ContinuousEvent: // ⚛️可连续事件
default:
listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
break;
}
const rawEventName = getRawEventName(topLevelType);
if (capture) { // 绑定事件处理器到元素
addEventCaptureListener(element, rawEventName, listener);
} else {
addEventBubbleListener(element, rawEventName, listener);
}
}
复制代码
事件绑定的过程还比较简单, 接下来看看事件是如何分发的。
按惯例仍是先上流程图:
经过上面的trapEventForPluginEventSystem
函数能够知道,不一样的事件类型有不一样的事件处理器, 它们的区别是调度的优先级不同:
// 离散事件
// discrentUpdates 在UserBlocking优先级中执行
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, nativeEvent) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
}
// 阻塞事件
function dispatchUserBlockingUpdate( topLevelType, eventSystemFlags, nativeEvent, ) {
// 若是开启了enableUserBlockingEvents, 则在UserBlocking优先级中调度,
// 开启enableUserBlockingEvents能够防止饥饿问题,由于阻塞事件中有scroll、mouseMove这类频繁触发的事件
// 不然同步执行
if (enableUserBlockingEvents) {
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(null, topLevelType, eventSystemFlags, nativeEvent),
);
} else {
dispatchEvent(topLevelType, eventSystemFlags, nativeEvent);
}
}
// 可连续事件则直接同步调用dispatchEvent
复制代码
最终不一样的事件类型都会调用dispatchEvent
函数. dispatchEvent
中会从DOM原生事件对象获取事件触发的target,再根据这个target获取关联的React节点实例.
export function dispatchEvent(topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent): void {
// 获取事件触发的目标DOM
const nativeEventTarget = getEventTarget(nativeEvent);
// 获取离该DOM最近的组件实例(只能是DOM元素组件)
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// ....
dispatchEventForPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst);
}
复制代码
接着(中间还有一些步骤,这里忽略)会调用EventPluginHub
的runExtractedPluginEventsInBatch
,这个方法遍历插件列表来处理事件,生成一个SyntheticEvent列表:
export function runExtractedPluginEventsInBatch( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, ) {
// 遍历插件列表, 调用插件的extractEvents,生成SyntheticEvent列表
const events = extractPluginEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 事件处理器执行, 见后文批量执行
runEventsInBatch(events);
}
复制代码
如今来看看插件是如何处理事件的,咱们以SimpleEventPlugin
为例:
const SimpleEventPlugin: PluginModule<MouseEvent> & {
getEventPriority: (topLevelType: TopLevelType) => EventPriority,
} = {
eventTypes: eventTypes,
// 抽取事件对象
extractEvents: function( topLevelType: TopLevelType, targetInst: null | Fiber, nativeEvent: MouseEvent, nativeEventTarget: EventTarget, ): null | ReactSyntheticEvent {
// 事件配置
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
// 1️⃣ 根据事件类型获取SyntheticEvent子类事件构造器
let EventConstructor;
switch (topLevelType) {
// ...
case DOMTopLevelEventTypes.TOP_KEY_DOWN:
case DOMTopLevelEventTypes.TOP_KEY_UP:
EventConstructor = SyntheticKeyboardEvent;
break;
case DOMTopLevelEventTypes.TOP_BLUR:
case DOMTopLevelEventTypes.TOP_FOCUS:
EventConstructor = SyntheticFocusEvent;
break;
// ... 省略
case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
// ...
case DOMTopLevelEventTypes.TOP_POINTER_UP:
EventConstructor = SyntheticPointerEvent;
break;
default:
EventConstructor = SyntheticEvent;
break;
}
// 2️⃣ 构造事件对象, 从对象池中获取
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 3️⃣ 根据DOM事件传播的顺序获取用户事件处理器
accumulateTwoPhaseDispatches(event);
return event;
},
};
复制代码
SimpleEventPlugin
的extractEvents
主要作如下三个事情:
为了不频繁建立和释放事件对象致使性能损耗(对象建立和垃圾回收),React使用一个事件池来负责管理事件对象,使用完的事件对象会放回池中,以备后续的复用。
这也意味着,在事件处理器同步执行完后,SyntheticEvent对象就会立刻被回收,全部属性都会无效。因此通常不会在异步操做中访问SyntheticEvent事件对象。你也能够经过如下方法来保持事件对象的引用:
SyntheticEvent#persist()
方法,告诉React不要回收到对象池SyntheticEvent#nativeEvent
, nativeEvent是能够持久引用的,不过为了避免打破抽象,建议不要直接引用nativeEvent构建完SyntheticEvent对象后,就须要遍历组件树来获取订阅该事件的用户事件处理器了:
function accumulateTwoPhaseDispatchesSingle(event) {
// 以_targetInst为基点, 按照DOM事件传播的顺序遍历组件树
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
复制代码
遍历方法其实很简单:
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) { // 从inst开始,向上级回溯
path.push(inst);
inst = getParent(inst);
}
let i;
// 捕获阶段,先从最顶层的父组件开始, 向下级传播
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
// 冒泡阶段,从inst,即事件触发点开始, 向上级传播
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
复制代码
accumulateDirectionalDispatches
函数则是简单查找当前节点是否有对应的事件处理器:
function accumulateDirectionalDispatches(inst, phase, event) {
// 检查是否存在事件处理器
const listener = listenerAtPhase(inst, event, phase);
// 全部处理器都放入到_dispatchListeners队列中,后续批量执行这个队列
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
复制代码
例以下面的组件树, 遍历过程是这样的:
最终计算出来的_dispatchListeners
队列是这样的:[handleB, handleC, handleA]
遍历执行插件后,会获得一个SyntheticEvent列表,runEventsInBatch
就是批量执行这些事件中的_dispatchListeners
事件队列
export function runEventsInBatch( events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null, ) {
// ...
forEachAccumulated(processingEventQueue, executeDispatchesAndRelease);
}
// 👇
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
if (event) {
// 按顺序执行_dispatchListeners
// 👇
executeDispatchesInOrder(event);
// 若是没有调用persist()方法则直接回收
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
export function executeDispatchesInOrder(event) {
// 遍历dispatchListeners
for (let i = 0; i < dispatchListeners.length; i++) {
// 经过调用 stopPropagation 方法能够禁止执行下一个事件处理器
if (event.isPropagationStopped()) {
break;
}
// 执行事件处理器
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
}
复制代码
OK, 到这里React的事件机制就基本介绍完了,这里只是简单了介绍了一下SimpleEventPlugin
, 实际代码中还有不少事件处理的细节,限于篇幅,本文就不展开去讲了。有兴趣的读者能够亲自去观摩React的源代码.
React内部有一个实验性的事件API,React内部称为React Flare
、正式名称是react-events
, 经过这个API能够实现跨平台、跨设备的高级事件封装.
react-events定义了一个**事件响应器(Event Responders)**的概念,这个事件响应器能够捕获子组件树或应用根节点的事件,而后转换为自定义事件.
比较典型的高级事件是press、longPress、swipe这些手势。一般咱们须要本身或者利用第三方库来实现这一套手势识别, 例如
import Gesture from 'rc-gesture';
ReactDOM.render(
<Gesture onTap={handleTap} onSwipe={onSwipe} onPinch={handlePinch} > <div>container</div> </Gesture>,
container);
复制代码
那么react-events的目的就是提供一套通用的事件机制给开发者来实现'高级事件'的封装, 甚至实现事件的跨平台、跨设备, 如今你能够经过react-events来封装这些手势事件.
react-events除了核心的Responder
接口,还封装了一些内置模块, 实现跨平台的、经常使用的高级事件封装:
举Press
模块做为例子, Press模块会响应它包裹的元素的press事件。press事件包括onContextMenu、onLongPress、onPress、onPressEnd、onPressMove、onPressStart等等. 其底层经过mouse、pen、touch、trackpad等事件来转换.
看看使用示例:
import { PressResponder, usePressListener } from 'react-events/press';
const Button = (props) => (
const listener = usePressListener({ // ⚛️ 经过hooks建立Responder
onPressStart,
onPress,
onPressEnd,
})
return (
<div listeners={listener}> {subtrees} </div>
);
);
复制代码
react-events的运做流程图以下, 事件响应器(Event Responders)会挂载到host节点,它会在host节点监听host或子节点分发的原生事件(DOM或React Native), 并将它们转换/合并成高级的事件:
你能够经过这个Codesanbox玩一下
react-events
:![]()
咱们挑一个简单的模块来了解一些react-events的核心API, 目前最简单的是Keyboard模块. Keyboard模块的目的就是规范化keydown和keyup事件对象的key属性(部分浏览器key属性的行为不同),它的实现以下:
/** * 定义Responder的实现 */
const keyboardResponderImpl = {
/** * 1️⃣定义Responder须要监听的子树的DOM事件,对于Keyboard来讲是['keydown', 'keyup';] */
targetEventTypes,
/** * 2️⃣监听子树触发的事件 */
onEvent(
event: ReactDOMResponderEvent, // 包含了当前触发事件的相关信息,如原生事件对象,事件触发的节点,事件类型等等
context: ReactDOMResponderContext, // Responder的上下文,给Responder提供了一些方法来驱动事件分发
props: KeyboardResponderProps, // 传递给Responder的props
): void {
const {responderTarget, type} = event;
if (props.disabled) {
return;
}
if (type === 'keydown') {
dispatchKeyboardEvent(
'onKeyDown',
event,
context,
'keydown',
((responderTarget: any): Element | Document),
);
} else if (type === 'keyup') {
dispatchKeyboardEvent(
'onKeyUp',
event,
context,
'keyup',
((responderTarget: any): Element | Document),
);
}
},
};
复制代码
再来看看dispatchKeyboardEvent:
function dispatchKeyboardEvent( eventPropName: string, event: ReactDOMResponderEvent, context: ReactDOMResponderContext, type: KeyboardEventType, target: Element | Document, ): void {
// ⚛️建立合成事件对象,在这个函数中会规范化事件的key属性
const syntheticEvent = createKeyboardEvent(event, context, type, target);
// ⚛️经过Responder上下文分发事件
context.dispatchEvent(eventPropName, syntheticEvent, DiscreteEvent);
}
复制代码
导出Responder:
// ⚛️createResponder把keyboardResponderImpl转换为组件形式
export const KeyboardResponder = React.unstable_createResponder(
'Keyboard',
keyboardResponderImpl,
);
// ⚛️建立hooks形式
export function useKeyboardListener(props: KeyboardListenerProps): void {
React.unstable_useListener(KeyboardResponder, props);
}
复制代码
如今读者应该对Responder的职责有了一些基本的了解,它主要作如下几件事情:
targetEventTypes
onEvent
context.dispatchEvent
和上面的Keyboard模块相比,现实中的不少高级事件,如longPress, 它们的实现则要复杂得多. 它们可能要维持必定的状态、也可能要独占响应的全部权(即同一时间只能有一个Responder能够对事件进行处理, 这个经常使用于移动端触摸手势,例如React Native的GestureResponderSystem)。
react-events目前都考虑了这些场景, 看一下API概览:
详细能够看react-events官方仓库
上文提到了React事件内部采用了插件机制,来实现事件处理和合成,比较典型的就是onChange事件。onChange事件其实就是所谓的‘高级事件’,它是经过表单组件的各类原生事件来模拟的。
也就是说,React经过插件机制本质上是能够实现高级事件的封装的。可是若是读者看过源代码,就会以为里面逻辑比较绕,并且依赖React的不少内部实现。因此这种内部的插件机制并非面向普通开发者的。
react-events
接口就简单不少了,它屏蔽了不少内部细节,面向普通开发者。咱们能够利用它来实现高性能的自定义事件分发,更大的意义是经过它能够实现跨平台/设备的事件处理方式.
目前react-events仍是实验阶段,特性是默认关闭,API可能会出现变动, 因此不建议在生产环境使用。能够经过这个Issue来关注它的进展。
最后赞叹一下React团队的创新能力!
完!