event 对象
1 ) 概述
- 在生产事件对象的过程当中,要去调用每一个
possiblePlugin.extractEvents
方法 - 现在单独看下这里面的细节过程,即如何去生产这个事件对象的过程
2 )源码
定位到 packages/events/EventPluginHub.js#L172
js
function extractEvents(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events = null;
for (let i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
// 这里要去调用 每个 plugin 的 extractEvents 方法
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
注意这里的 possiblePlugin.extractEvents
我们专门专注下 changeEvent 的这个方法
定位到 packages/react-dom/src/events/ChangeEventPlugin.js#L263
js
const ChangeEventPlugin = {
eventTypes: eventTypes,
_isInputEventSupported: isInputEventSupported,
// 注意这里
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 在这个方法里面,拿到了 targetNode
// 因为这边传进来的呢是一个发布对象, 所以要通过这种方法拿到它的 node
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
// 然后,要经过一系列的判断,主要去赋值了不同的 getTargetInstFunc
let getTargetInstFunc, handleEventFunc;
// 如果有返回这个instance,那么我们就可以去创建这个event了,这是什么意思呢?
// 就是说我们这个 event plugin, 在所有的事件触发的过程当中,这个plugin都会被循环调用的
// 它是没有通过事件名称来调用不同的plugin这么一个设置的
// 所以这个判断是要放在每个pluggin里面自己去做。就是说根据这次触发的具体事件是什么?
// 来判断我们要不要为它创建一个event,因为每个plugin在每次事件触发都会被调用
// 如果我们都生成事件,那么明显是不对的,肯定要对自己这个 plugin 关心的事件来去为它生成这个事件
if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(targetNode)) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
// polyfill 的这些先忽略
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
}
// 基于类型,得到了最终的处理函数
if (getTargetInstFunc) {
const inst = getTargetInstFunc(topLevelType, targetInst);
if (inst) {
// 创建 event
const event = createAndAccumulateChangeEvent(
inst,
nativeEvent,
nativeEventTarget,
);
return event;
}
}
if (handleEventFunc) {
handleEventFunc(topLevelType, targetNode, targetInst);
}
// When blurring, set the value attribute for number inputs
if (topLevelType === TOP_BLUR) {
handleControlledInputBlur(targetNode);
}
},
};
-
进入
shouldUseChangeEvent
jsfunction shouldUseChangeEvent(elem) { const nodeName = elem.nodeName && elem.nodeName.toLowerCase(); return ( nodeName === 'select' || (nodeName === 'input' && elem.type === 'file') ); }
-
这个判断其实就是主要来判断一下我们这个节点上面是否有changeevent
-
并且是否应该用 change event 来进行一个触发
-
因为react当中的 onchange 事件,其实它是封装了各种不同的事件的
-
比如说对于像我们输入文本的 input type='text' 的一个情况
-
正常来讲,应该绑定的是input事件,而不是change事件
-
因为change事件在有些浏览器里面要等到这个输入框,blur的时候,才会真正触发这个change事件
-
对于input事件是我们每次有输入变化的时候,都会触发的这个事件
-
所以对于 select 还有 input type='file', 它们的change是非常明显的,就是等到它们有内容变化的时候就会触发
-
因为file是我们选择了一个文件之后,它就会触发change事件
-
而select我们选择了某一个 option 之后,它也会触发这个change事件
-
所以对于这种节点,我们可以直接使用onchange来进行一个绑定
-
这时候,
getTargetInstFunc = getTargetInstForChangeEvent;
进入getTargetInstForChangeEvent
jsfunction getTargetInstForChangeEvent(topLevelType, targetInst) { // TOP_CHANGE 就是 change if (topLevelType === TOP_CHANGE) { return targetInst; } } // 注意 另外的文件中 // packages/react-dom/src/events/DOMTopLevelEventTypes.js#L41 export const TOP_CHANGE = unsafeCastStringToDOMTopLevelType('change'); // packages/events/TopLevelEventTypes.js#L27 export function unsafeCastStringToDOMTopLevelType( topLevelType: string, ): DOMTopLevelEventType { return topLevelType; }
-
-
进入
isTextInputElement
jsconst supportedInputTypes: {[key: string]: true | void} = { color: true, date: true, datetime: true, 'datetime-local': true, email: true, month: true, number: true, password: true, range: true, search: true, tel: true, text: true, time: true, url: true, week: true, }; function isTextInputElement(elem: ?HTMLElement): boolean { // 获取 nodeName const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase(); // 判断在 input 的时候,是否符合支持的type类型 if (nodeName === 'input') { return !!supportedInputTypes[((elem: any): HTMLInputElement).type]; } if (nodeName === 'textarea') { return true; } return false; }
- 这里就是一个 boolean 类型的函数进行类型判断的
-
进入
getTargetInstForInputOrChangeEvent
js// packages/react-dom/src/events/ChangeEventPlugin.js#L229 function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) { if (topLevelType === TOP_INPUT || topLevelType === TOP_CHANGE) { return getInstIfValueChanged(targetInst); } } // packages/react-dom/src/events/ChangeEventPlugin.js#L106 function getInstIfValueChanged(targetInst) { const targetNode = getNodeFromInstance(targetInst); if (inputValueTracking.updateValueIfChanged(targetNode)) { return targetInst; } } // packages/react-dom/src/client/ReactDOMComponentTree.js#L69 export function getNodeFromInstance(inst) { if (inst.tag === HostComponent || inst.tag === HostText) { // In Fiber this, is just the state node right now. We assume it will be // a host component or host text. return inst.stateNode; } // Without this first invariant, passing a non-DOM-component triggers the next // invariant for a missing parent, which is super confusing. invariant(false, 'getNodeFromInstance: Invalid argument.'); }
-
进入
shouldUseClickEvent
js/** * SECTION: handle `click` event */ // checkbox 和 radio 的特殊处理 function shouldUseClickEvent(elem) { // Use the `click` event to detect changes to checkbox and radio inputs. // This approach works across all browsers, whereas `change` does not fire // until `blur` in IE8. const nodeName = elem.nodeName; return ( nodeName && nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio') ); }
-
进入
getTargetInstForClickEvent
jsfunction getTargetInstForClickEvent(topLevelType, targetInst) { if (topLevelType === TOP_CLICK) { return getInstIfValueChanged(targetInst); } }
-
进入
createAndAccumulateChangeEvent
创建事件对象jsfunction createAndAccumulateChangeEvent(inst, nativeEvent, target) { const event = SyntheticEvent.getPooled( eventTypes.change, inst, nativeEvent, target, ); event.type = 'change'; // Flag this event loop as needing state restore. enqueueStateRestore(target); accumulateTwoPhaseDispatches(event); return event; }
-
这个函数就是具体生成这个事件的一个过程
- 可以看到这个事件,接收了一个 inst,然后传入了 nativeEvent,并且再传入 target
- 然后,通过
SyntheticEvent.getPooled
,就是说在react当中所有的事件对象是通过一个 pool 来进行一个存储的 - 比如说我们为所有的event创建了十个event对象
- 每一次有新的一个event进来的时候,从这个pool里面拿出一个设置一些事件以及对应的一些值之后
- 去触发每一个事件的监听方法,然后去使用这个 event 对象
- 这个 event 对象使用完了之后,又会归还到这个 pool 里面
- 也就是一个 能够减少 对象声明 以及 对象回收 的一个性能开销
- 然后拿到了这个 event 之后,给它设置了 type 是 change
- 之后执行两个函数
enqueueStateRestore
和accumulateTwoPhaseDispatches
-
进入
SyntheticEvent.getPooled
js// packages/events/SyntheticEvent.js#L335 function addEventPoolingTo(EventConstructor) { EventConstructor.eventPool = []; EventConstructor.getPooled = getPooledEvent; // 注意这里 EventConstructor.release = releasePooledEvent; }
-
进入
getPooledEvent
js// packages/events/SyntheticEvent.js#L300 function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) { const EventConstructor = this; // 存在poll if (EventConstructor.eventPool.length) { const instance = EventConstructor.eventPool.pop(); EventConstructor.call( instance, dispatchConfig, targetInst, nativeEvent, nativeInst, ); return instance; } // pool 里面没有,则创建一个新的 return new EventConstructor( dispatchConfig, targetInst, nativeEvent, nativeInst, ); }
- 关于
EventConstructor
- 首先在 packages/events/SyntheticEvent.js#L62 中的
SyntheticEvent
构造方法 - 要理解这个过程, 首先在这个js里面先声明了一个叫做 SyntheticEvent 这么一个方法
- 这个方法它是一个constructor 方法, 在这个方法里面去声明事件相关的各种属性
- 重点关注它的事件的一个触发的过程以及生产的过程,所以只关心它的 pool 的处理过程
- 首先在 packages/events/SyntheticEvent.js#L62 中的
- 这里的 this 就是
SyntheticEvent
- 在这个构造方法的原型链上也有一大堆的东西,对事件对象进行一个封装和扩展
- 注意这里的
EventConstructor.call
和new EventConstructor
都达到同一个目的
- 关于
-
-
进入
enqueueStateRestore
jsexport function enqueueStateRestore(target: EventTarget): void { // 判断了这个 restoreTarget 公共变量是否存在 if (restoreTarget) { if (restoreQueue) { restoreQueue.push(target); } else { restoreQueue = [target]; } // restoreTarget 不存在,则对其进行赋值 } else { restoreTarget = target; } }
- 作用是处理,如果setState之后,这个 state 对应的 input 的 value 是不一样的
- 要把这个值进行一个回滚
-
进入
accumulateTwoPhaseDispatches
这个方法才是真正要去从每个节点上面去获取它的 listener 的一个过程js// packages/events/EventPropagators.js#L115 export function accumulateTwoPhaseDispatches(events) { // 其实就是对 events 这个数组里面,它的每一个节点去调用这个方法 forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); } function accumulateTwoPhaseDispatchesSingle(event) { // 存在 phasedRegistrationNames 则调用 traverseTwoPhase if (event && event.dispatchConfig.phasedRegistrationNames) { traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); } } // 在 event 对象上 插入 listener 的过程 function accumulateDirectionalDispatches(inst, phase, event) { // 忽略 if (__DEV__) { warningWithoutStack(inst, 'Dispatching inst must not be null'); } // 获取 listener const listener = listenerAtPhase(inst, event, phase); if (listener) { // 注意这里 event._dispatchListeners 和 下面的 event._dispatchInstances 保持两者一一对应的关系 event._dispatchListeners = accumulateInto( event._dispatchListeners, listener, ); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(inst, registrationName); } // packages/events/EventPluginHub.js#L126 export function getListener(inst: Fiber, registrationName: string) { let listener; // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not // live here; needs to be moved to a better place soon const stateNode = inst.stateNode; if (!stateNode) { // Work in progress (ex: onload events in incremental mode). return null; } const props = getFiberCurrentPropsFromNode(stateNode); // 从 dom tree上获取 props if (!props) { // Work in progress. return null; } listener = props[registrationName]; if (shouldPreventMouseEvent(registrationName, inst.type, props)) { return null; } invariant( !listener || typeof listener === 'function', 'Expected `%s` listener to be a function, instead got a value of `%s` type.', registrationName, typeof listener, ); return listener; } // packages/shared/ReactTreeTraversal.js#L86 export function traverseTwoPhase(inst, fn, arg) { const path = []; // 找到所有上层节点,并存入 path while (inst) { path.push(inst); inst = getParent(inst); } // 下面是核心,执行两个阶段的回调,捕获和冒泡 let i; for (i = path.length; i-- > 0; ) { // 注意这个 i 的顺序 // path[i]:节点, arg:event fn(path[i], 'captured', arg); // captured 是从 window 向下触发的 } for (i = 0; i < path.length; i++) { // 注意这个 i 的顺序 fn(path[i], 'bubbled', arg); // bubbled 是从下向 window 方向的 } // 基于上面两个 循环 // 这样的话,就不需要在 event 对象上面单独维护 capture 的这个事件的它的一个数组 // 还有 bubble 的事件的一个速度,只需要放在同一个数组里面,然后按照这个数组的顺序去触发就可以了 } // packages/shared/ReactTreeTraversal.js#L10 function getParent(inst) { do { inst = inst.return; // TODO: If this is a HostRoot we might want to bail out. // That is depending on if we want nested subtrees (layers) to bubble // events to their parent. We could also go through parentNode on the // host node but that wouldn't work for React Native and doesn't let us // do the portal feature. } while (inst && inst.tag !== HostComponent); if (inst) { return inst; // 返回的 inst 是一个 HostComponent } return null; }
-
-
以上,生产 event 对象,然后去挂载它的事件,这个过程是非常的复杂的
-
react 团队把整个事件系统去重新抽象的这么一个过程,而且设计的超级复杂
-
这一套东西只是非常适合react,在其他框架要使用这类event库,会有很大的成本
-
目前为止,通过 ChangeEventPlugin 来了解了整个 event 对象的处理过程
-
后续其他的类似事件的处理逻辑到后面都是一样的
-
但每一个 plugin 或多或少有一些自己的一些区别,这里不再赘述