React | 2020-08-25 22:33:27 802次 1次
react 中实现了一套事件系统,将事件绑定到 document 上,主要为了跨平台以及事件优先级的调度,还可以兼容不同浏览器等优点。
一、总体设计
/**
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|PluginRegistry| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
*/
【ReactEventListener 】
概念: 负责给元素绑定事件
【ReactEventEmitter】
概念: 暴露接口给 React 组件层用于添加事件订阅(对外暴露了 listenTo 等方法)
【EventPluginHub】
概念:负责管理和注册各种插件
【plugin 插件】
SimpleEventPlugin:blur、focus、click、submit、touchMove、mouseMove、scroll、drag、load
EnterLeaveEventPlugin:mouseEnter/mouseLeave 和 pointerEnter/pointerLeave
ChangeEventPlugin: onChange(比较复杂)
SelectEventPlugin:select选择
BeforeInputEventPlugin
...
执行过程:事件绑定 ---> 合成事件列表获取 --> 事件触发。
二、事件分类和优先级
DiscreteEvent(离散事件):click,blur,focus,submit,tuchStart 等,优先级是 0。
UserBlockingEvent(用户阻塞事件):touchMove,mouseMove,scroll,drag,dragOver 等,这些事件会阻塞用户的交互,优先级是 1。
ContinuousEvent(连续事件):load,error,loadStart,abort,animationend 等,优先级是 2,这个优先级最高,不会被打断。
优先级:
Immediate - 这个优先级的任务会同步执行, 或者说要马上执行且不能中断-----------对应【ContinuousEvent】
UserBlocking(250ms timeout) 这些任务一般是用户交互的结果, 需要即时得到反馈 .----对应【UserBlockingEvent、DiscreteEvent】
Normal (5s timeout) 应对哪些不需要立即感受到的任务,例如网络请求
Low (10s timeout) 这些任务可以放后,但是最终应该得到执行. 例如分析通知
Idle (no timeout) 一些没有必要做的任务 (比如隐藏的内容).
三、事件绑定
事件绑定大致流程:
complate
|
finalizeInitialChildren
|
setInitialProperties
|
setInitialDOMProperties 根据不同的属性类别进行操作,如果是事件类型才会继续往下走
|
ensureListeningTo
|
legacyListenToEvent
|
legacyListenToTopLevelEvent
|
trapBubbledEvent | trapCapturedEvent 捕获或者冒泡
|
trapEventForPluginEventSystem 根据优先级调度
|
addEventCaptureListener 监听
属性遍历
setInitialDOMProperties 方法会遍历所有的属性,执行对应的属性处理:
function setInitialDOMProperties(...): void {
// 一个大循环遍历所有属性
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// style 属性设置
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
...setInnerHTML(domElement, nextHtml);
} else if (propKey === CHILDREN) {
...
// string | number 才会设置setTextContent
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// 绑定了事件,进行下一步操作
ensureListeningTo(rootContainerElement, propKey);
}
} else if (nextProp != null) {
// 其他就是设置属性值
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
这里有个 registrationNameModules,这个就是插件的一个注册过程,这个过程是在页面加载时候就会执行,里面记录一些固定的事件写法,所以可以判断是否为事件类型,大致过程在下一小节。
事件分发(冒泡 | 捕获)
在 legacyListenToTopLevelEvent 中会对一些不冒泡的事件直接进行 trapCapturedEvent,不会冒泡的事件:
- scroll
- blur & focus
- Media 事件
- mouseleave & mouseenter
其他的事件进行 trapBubbledEvent
但是代码中可以看到没有对 Form 事件和 Media 事件进行委托,因为这些事件委托后会触发两次回调函数:
function legacyListenToTopLevelEvent(...): void {
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
...
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
break;
default:
// 不是 media 事件才会冒泡
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt);
}
break;
}
}
}
优先级事件创建
这一步主要是创建出不同优先级的事件回调,监听后等待被执行:
function trapEventForPluginEventSystem(
container: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
capture: boolean,
): void {
let listener;
// topLevelType 就是特定的事件名称,反查一下
switch (getEventPriorityForPluginSystem(topLevelType)) {
case DiscreteEvent:
// 一个开关控制,等待上一个执行完毕再执行当前事件
listener = dispatchDiscreteEvent.bind(...);
break;
case UserBlockingEvent:
// 会同步执行
listener = dispatchUserBlockingUpdate.bind(...);
break;
case ContinuousEvent:
default:
// 立即执行
listener = dispatchEvent.bind(...);
break;
}
// 创建监听到root,创建出的listener作为回调
if (capture) {
addEventCaptureListener(container, rawEventName, listener);
} else {
addEventBubbleListener(container, rawEventName, listener);
}
}
最终这里是要创建出一个 listener(dispatchEvent) 回调,其中 DiscreteEvent 需要单独包装一下,按照顺序异步执行,其他两个同步执行,里面具体做了什么等下面调用时候再看。
事件注册的过程结束。
四、插件注册
插件注册的过程是独立的,页面加载完就会去执行,大致流程:
injectEventPluginsByName
|
recomputePluginOrdering
|
publishEventForPlugin
|
publishRegistrationName -- 给 registrationNameModules 赋值 {onClick: {}, ...}
参数主要是由入口函数传递进入的:
injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
SimpleEventPlugin 数据结构
传入不同的事件插件,其中 SimpleEventPlugin 的数据结构如下:
{
eventTypes: {
phasedRegistrationNames: {
bubbled: 'onClick', //冒泡
captured: 'onClick' + 'Capture', //捕获
},
dependencies: ['onClick'],
eventPriority: 1, //优先级
},
extractEvents: () => {}
}
初始的执行会创建出一套这样的针对不同平台的事件对象(具体方法由个平台传入,本次看到的是 dom 层面定义的一些东西,具体的创建代码简要贴一下):
const discreteEventPairsForSimpleEventPlugin = [
DOMTopLevelEventTypes.TOP_CLICK, 'click',
DOMTopLevelEventTypes.TOP_CLOSE, 'close',
...
]
function processSimpleEventPluginPairsByPriority(...): void {
for (let i = 0; i < eventTypes.length; i += 2) {
// 指定的结构
const config = {
phasedRegistrationNames: {
bubbled: onEvent,
captured: onEvent + 'Capture',
},
dependencies: [topEvent],
eventPriority: priority,
};
// 这个值给到 eventTypes 上,并最终创建出 SimpleEventPlugin 的事件列表
simpleEventPluginEventTypes[event] = config;
}
}
// 初次执行这个的时候就已经确定好了优先级,因为事件和优先级关系就是react指定的
processSimpleEventPluginPairsByPriority(discreteEventPairsForSimpleEventPlugin, DiscreteEvent)
extractEvents 合成事件
extractEvents 方法会等到下个阶段事件执行(extractPluginEvents)时候再被触发,大致逻辑:
...
const event = EventConstructor.getPooled(...);
// 模拟捕获和传播
accumulateTwoPhaseDispatches(event);
// 返回这个事件,后面使用
return event;
...
EventConstructor 来自 SyntheticEvent 基类,这个类可以被扩展,其实就是 extend 方法中实现一个继承机制,这样一些其他事件可以使用基类一些公共方法,比如上面的 getPooled 方法,是从对象池中获取数据,避免对象的重复创建,减少 GC 开销。
模拟捕获和冒泡事件过程,最终挂载到 event._dispatchListeners 上,它存储了所有需要触发的监听函数,比如父子都绑定了事件,这里需要全部记录,从而可以模拟冒泡和捕获的执行:
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) {
path.push(inst);
// 往上寻找
inst = getParent(inst);
}
let i;
// 这个过程创建的对象最终挂载到 event._dispatchListeners 上
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
举例:
<div onClick = { A }>
<div onClickCapture = { B }>
<div onClick = { C }>
</div>
</div>
</div>
最终 event._dispatchListeners 顺序为 [ B, C, A ] 因为中间这个事件定义为 【捕获阶段触发】
----------------------------------------------------------------------
<div onClick = { A }>
<div onClick = { B }>
<div onClick = { C }>
</div>
</div>
</div>
最终 event._dispatchListeners 顺序为 [ C, B, A ] 默认为 【冒泡阶段触发事件】
五、事件执行
大致流程:
dispatchEvent
|
attemptToDispatchEvent
|
dispatchEventForLegacyPluginEventSystem
|
batchedEventUpdates 批量更新合成事件,同一个dom触发的多个合成事件
|
batchedEventUpdates$1
|
handleTopLevel
|
runExtractedPluginEventsInBatch
|
extractPluginEvents 执行上面插件方法,创建出一个合成事件列表
【SimpleEventPlugin->extractEvents】触发插件
|
accumulateInto
|
上面会取到一个合成事件列表,批量执行
runEventsInBatch
|
executeDispatchesInOrder
|
executeDispatch
|
finishEventHandler
|
如果此时发生数据改变则开始调度
runWithPriority$1
Scheduler_runWithPriority
flushSyncCallbackQueue
事件执行的时候做的事情很多,比如事件监听回调中做了什么,插件中预留的那个方法进行事件列表处理,最终怎么执行,又如何调度更新等问题,逐步来展开分析。
dispatchEvent回调
dispatchEvent 中调用了 attemptToDispatchEvent:
// 这里接收四个参数,在dispatchDiscreteEvent.bind(...) 中传入了四个参数
// 第一个为绑定对象 null,剩余三个 topLevelType PLUGIN_EVENT_SYSTEM container(document)
// 事件监听触发时传入第四个参数,就是触发的目标节点 nativeEvent
export function attemptToDispatchEvent(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
container: Document | Element | Node,
nativeEvent: AnyNativeEvent,
){
// 从目标事件身上取到 target真实dom
const nativeEventTarget = getEventTarget(nativeEvent);
// 根据真实dom获取fiber对象 internalInstanceKey:一个字符串+随机数
// createInstance时候创建关系 precacheFiberNode方法中
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
...
...
dispatchEventForLegacyPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
);
return null;
}
这个方法中第四个参数是执行事件监听回调时传入,获取对应的 target 之后,再根据真实 dom 取出对应的 fiber 节点对象,fiber 和真实 dom 的关系创建在 createInstance 时候创建。
继续进入 dispatchEventForLegacyPluginEventSystem:
export function dispatchEventForLegacyPluginEventSystem(...): void {
const bookKeeping = getTopLevelCallbackBookKeeping(...);
try {
// 批量更新
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
这个过程中代码很少,但是进入之后的各种回调相当复杂,获取到 bookKeeping 事件列表之后进行批量执行 _dispatchListeners。
批量执行
可以看到获取 event._dispatchListeners 之后,需要对当前触发点整条链路相关的节点注册的事件进行批量执行,注意和批量更新的区别。
在第四部分那里得知,执行完 合成事件 创建之后,会返回这个 event 对象,从 runEventsInBatch 方法开始:
function runEventsInBatch(events) {
if (events !== null) {
// 这个方法和插件注册执行那里一致,取到一个合成事件列表
eventQueue = accumulateInto(eventQueue, events);
}
var processingEventQueue = eventQueue;
...
// 这个方法就是一个遍历,执行传入的回调
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
...
}
// executeDispatchesAndReleaseTopLevel 方法执行
var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) {
if (event) {
executeDispatchesInOrder(event);
// 执行完事件回调后,如果事件没有调用 persist 方法,那就释放对象,
// 但是释放后的对象上属性值都为 null,此时如果数据池不满则放回数据池
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
executeDispatchesInOrder 方法中会判断是否阻止冒泡,并执行掉事件:
function executeDispatchesInOrder(event) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
// 阻止冒泡
// 开发者主动调用e.stopPropagation(),react将isPropagationStopped设置为返回 true 的一个函数
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
// 单个事件
executeDispatch(event, dispatchListeners, dispatchInstances);
}
// 重置
event._dispatchListeners = null;
event._dispatchInstances = null;
}
最终在 executeDispatch 方法中执行 dispatchListeners 回调(也就是绑定的合成事件)
六、总结
react 事件系统的原理进行了一个大致的梳理,其中有一些细节的实现没有详细展开的分析,比如事件池的使用、其他类型事件,等有时间再逐步补充,尤其是 onChange 事件的设计很复杂。
1人赞