React16源码: React中event事件监听绑定的源码实现
event事件监听
1 )概述
- 在 react-dom 代码初始化的时候,去注入了平台相关的事件插件
- 接下去在react的更新过程绑定了事件的操作,在执行到 completeWork 的时候
- 对于 HostComponent 会一开始就先去执行了
finalizeInitialChildren
这个方法 - 位置在 packages/react-reconciler/src/ReactFiberCompleteWork.js#L642
2 )源码
定位到 packages/react-dom/src/client/ReactDOMHostConfig.js#L212
找到 finalizeInitialChildren
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}
定位到 packages/react-dom/src/client/ReactDOMComponent.js#L447
找到 setInitialProperties
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
if (__DEV__) {
validatePropertiesInDevelopment(tag, rawProps);
if (
isCustomComponentTag &&
!didWarnShadyDOM &&
(domElement: any).shadyRoot
) {
warning(
false,
'%s is using shady DOM. Using shady DOM with React can ' +
'cause things to break subtly.',
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
);
didWarnShadyDOM = true;
}
}
// TODO: Make sure that we check isMounted before firing any of these events.
let props: Object;
switch (tag) {
case 'iframe':
case 'object':
trapBubbledEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
}
props = rawProps;
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement);
trapBubbledEvent(TOP_SUBMIT, domElement);
props = rawProps;
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement);
props = rawProps;
break;
case 'input':
ReactDOMInput.initWrapperState(domElement, rawProps);
props = ReactDOMInput.getHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMOption.validateProps(domElement, rawProps);
props = ReactDOMOption.getHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelect.initWrapperState(domElement, rawProps);
props = ReactDOMSelect.getHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextarea.initWrapperState(domElement, rawProps);
props = ReactDOMTextarea.getHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
default:
props = rawProps;
}
assertValidProps(tag, props);
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.track((domElement: any));
ReactDOMInput.postMountWrapper(domElement, rawProps, false);
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.track((domElement: any));
ReactDOMTextarea.postMountWrapper(domElement, rawProps);
break;
case 'option':
ReactDOMOption.postMountWrapper(domElement, rawProps);
break;
case 'select':
ReactDOMSelect.postMountWrapper(domElement, rawProps);
break;
default:
if (typeof props.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
}
- 对于
iframe
,object
,video
,audio
,source
这些多媒体节点的初始化绑定 - 是通过
trapBubbledEvent
来实现的 - 后续执行到
setInitialDOMProperties
, 在这个方法内部function setInitialDOMProperties( tag: string, domElement: Element, rootContainerElement: Element | Document, nextProps: Object, isCustomComponentTag: boolean, ): void { for (const propKey in nextProps) { if (!nextProps.hasOwnProperty(propKey)) { continue; } const nextProp = nextProps[propKey]; if (propKey === STYLE) { // ... 跳过很多代码 } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { // ... 跳过很多代码 } else if (propKey === AUTOFOCUS) { // We polyfill it separately on the client during commit. // We could have excluded it in the property list instead of // adding a special case here, but then it wouldn't be emitted // on server rendering (but we *do* want to emit it in SSR). // 注意这里, propKey 是 dom 节点内的 props的配置,如果这个配置在 registrationNameModules 这里 // registrationNameModules 是通过每一个插件里面每一个 eventTypes 里面 // 它对应的有 phasedRegistrationNames 的情况下,比如说 onChange, onChangeCapture, 它都是作为它的一个key而存在的 // 也就是说我们如果在这个 props 上面写了 onChange onClick 这些事件相关的props的话 // 就会符合这个条件的判断,符合这个条件判断之后, 它会调用一个方法叫做 ensureListeningTo } else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } ensureListeningTo(rootContainerElement, propKey); // rootContainerElement 是 fiberRoot 对应的 container } } else if (nextProp != null) { // ... 跳过很多代码 } } }
- 进入
ensureListeningTo
function ensureListeningTo(rootContainerElement, registrationName) { const isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; // 如果它是一个document或者fragment,那么它就等于 rootContainerElement // 如果它不是,它就等于它的 rootContainerElement.ownerDocument // 这个是用来最终要去把事件绑定在哪个地方的 // 可以确定的是在 react 当中大部分可冒泡的事件都是通过事件代理的形式来进行一个绑定的 // 也就是说,不是每一个节点都会绑定自己的事件 // 因为每个节点绑定自己的事件,肯定是性能比较低下的一个操作,而且有可能会导致内存溢出这种情况 const doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument; // 调用这个方法 listenTo(registrationName, doc); }
- 进入
listenTo
// packages/react-dom/src/events/ReactBrowserEventEmitter.js#L126 export function listenTo( registrationName: string, mountAt: Document | Element, ) { // 注意这里 const isListening = getListeningForDocument(mountAt); const dependencies = registrationNameDependencies[registrationName]; // 遍历依赖 for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; // 没有这些依赖,则对 dependency 进行事件监听处理 if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { switch (dependency) { case TOP_SCROLL: // 这个方法监听的是 捕获阶段的事件 trapCapturedEvent(TOP_SCROLL, mountAt); break; case TOP_FOCUS: case TOP_BLUR: trapCapturedEvent(TOP_FOCUS, mountAt); trapCapturedEvent(TOP_BLUR, mountAt); // We set the flag for a single dependency later in this function, // but this ensures we mark both as attached rather than just one. isListening[TOP_BLUR] = true; isListening[TOP_FOCUS] = true; break; case TOP_CANCEL: case TOP_CLOSE: if (isEventSupported(getRawEventName(dependency))) { trapCapturedEvent(dependency, mountAt); } break; case TOP_INVALID: case TOP_SUBMIT: case TOP_RESET: // We listen to them on the target DOM elements. // Some of them bubble so we don't want them to fire twice. break; // 对于其他大部分的事件处理 用冒泡处理 default: // By default, listen on the top level to all non-media events. // Media events don't bubble so adding the listener wouldn't do anything. const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; // 注意这里排除了 mediaEventTypes,因为一开始就已经对一些 媒体事件处理了 if (!isMediaEvent) { trapBubbledEvent(dependency, mountAt); // 这是对常规事件的处理 冒泡 } break; } isListening[dependency] = true; } } }
-
进入
getListeningForDocument
const alreadyListeningTo = {}; let reactTopListenersCounter = 0; // 这个属性就是用来挂载 container 节点上面去记录这个节点监听了哪些事件的 // 用这种方式判断是因为 可能不存在这个属性,如果没有,则需要初始化属性 const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2); function getListeningForDocument(mountAt: any) { // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty` // directly. if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) { mountAt[topListenersIDKey] = reactTopListenersCounter++; // 这里初始化属性 alreadyListeningTo[mountAt[topListenersIDKey]] = {}; } // 那如果已经有了,我们就返回这个对象, 用来记录这个dom节点它是否监听了哪些事件的 return alreadyListeningTo[mountAt[topListenersIDKey]]; }
-
对于
mediaEventTypes
和媒体相关的事件// packages/react-dom/src/events/DOMTopLevelEventTypes.js#L155 export const mediaEventTypes = [ TOP_ABORT, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, TOP_DURATION_CHANGE, TOP_EMPTIED, TOP_ENCRYPTED, TOP_ENDED, TOP_ERROR, TOP_LOADED_DATA, TOP_LOADED_METADATA, TOP_LOAD_START, TOP_PAUSE, TOP_PLAY, TOP_PLAYING, TOP_PROGRESS, TOP_RATE_CHANGE, TOP_SEEKED, TOP_SEEKING, TOP_STALLED, TOP_SUSPEND, TOP_TIME_UPDATE, TOP_VOLUME_CHANGE, TOP_WAITING, ];
-
进入
trapCapturedEvent
export function trapCapturedEvent( topLevelType: DOMTopLevelEventType, element: Document | Element, ) { if (!element) { return null; } // 注意这里,根据是否是 Interactive 类型的事件,调用的不同的回调,最终赋值给 dispatch const dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; addEventCaptureListener( element, getRawEventName(topLevelType), // Check if interactive and wrap in interactiveUpdates dispatch.bind(null, topLevelType), ); }
- 进入
isInteractiveTopLevelEventType
const SimpleEventPlugin: PluginModule<MouseEvent> & { isInteractiveTopLevelEventType: (topLevelType: TopLevelType) => boolean, } = { eventTypes: eventTypes, // 注意这里的 topLevelEventsToDispatchConfig 一开始是一个空的对象 // 在调用 addEventTypeNameToConfig 时候加入的 // 这个方法是检测 isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean { const config = topLevelEventsToDispatchConfig[topLevelType]; return config !== undefined && config.isInteractive === true; }, // ... 跳过其他 }
- 进入
addEventTypeNameToConfig
function addEventTypeNameToConfig( [topEvent, event]: EventTuple, isInteractive: boolean, ) { const capitalizedEvent = event[0].toUpperCase() + event.slice(1); const onEvent = 'on' + capitalizedEvent; // 注意这个数据结构 const type = { phasedRegistrationNames: { bubbled: onEvent, captured: onEvent + 'Capture', }, dependencies: [topEvent], isInteractive, // 注意这里的标识 }; eventTypes[event] = type; topLevelEventsToDispatchConfig[topEvent] = type; // 这里进行注入 }
- 关于这里的
isInteractive
标识的来源interactiveEventTypeNames.forEach(eventTuple => { addEventTypeNameToConfig(eventTuple, true); }); nonInteractiveEventTypeNames.forEach(eventTuple => { addEventTypeNameToConfig(eventTuple, false); });
- 其中
interactiveEventTypeNames
- 参考 packages/react-dom/src/events/SimpleEventPlugin.js#L59
- 其中
nonInteractiveEventTypeNames
- 参考 packages/react-dom/src/events/SimpleEventPlugin.js#L95
- 上面两个数组对应 dom 原生的事件, 它们的区别是什么呢
- 这些事件去调用了设置的事件回调之后,里面如果有 setState
- 那么创建了update去计算的 expirationTime 会有 interactive 和 nonInteractive 的区分
- 它们的区别在 expirationTime,interactive的会比较的小
- 也就是说它的优先级会比较的高,它需要优先被执行
- 因为它是一个用户交互相关的事件,希望是用户比如说点了一个按钮之后
- 立马可以得到反馈, 因为它需要被优先执行的
- 其中
- 关于这里的
- 进入
- 进入
addEventCaptureListener
// 两者区别是第三个参数,bubble 是 false, capture 是 true export function addEventBubbleListener( element: Document | Element, eventType: string, listener: Function, ): void { element.addEventListener(eventType, listener, false); } // 注意这里 export function addEventCaptureListener( element: Document | Element, eventType: string, listener: Function, ): void { element.addEventListener(eventType, listener, true); // 主要是绑定 dom 原生事件 }
- 进入
-
同样对于
trapBubbledEvent
也同上类似,这里不再赘述
-
- 进入
- 进入