React | 2020-08-25 22:33:27 739次 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人赞