需求描述node
点击按钮弹出一个对话框,再次点按钮关闭对话框。点击对话框外的空白区域也能够关闭对话框。react
代码实现数组
class Demo extends PureComponent {
state = {
visible: false,
};
componentDidMount() {
document.body.addEventListener('click', () => {
this.setState({
visible: false,
});
});
}
componentWillUnmount() {
document.body.removeEventListener('click');
}
handleBtnClick = (e) => {
e.preventDefault();
const { visible } = this.state;
this.setState({
visible: !visible,
});
}
handleDialogClick = (e) => {
e.preventDefault();
}
render() {
const { visible } = this.state;
return (
<div>
<div
onClick={this.handleDialogClick}
style={{
display: visible ? 'block' : 'none',
position: 'fixed',
top: 100,
left: '50%',
marginLeft: -190,
width: 380,
height: 300,
background: '#fff',
zIndex: 999
}}
>
喵喵~~~
</div>
<Button onClick={this.handleBtnClick}>{visible ? 'close' : 'open'}</Button>
</div>
);
}
}
复制代码
很完美有没有?简直毫无破绽[捂脸]浏览器
但实际上的效果并非咱们想要的,点击 Dialog 依旧会关闭。缓存
能够作以下修改bash
一、经过 e.target 判断。app
class Demo extends PureComponent {
state = {
visible: false,
};
componentDidMount() {
document.body.addEventListener('click', (e) => {
if (e.target && (e.target.matches('.dialog') || e.target.matches('.btn'))) {
return;
}
this.setState({
visible: false,
});
});
}
componentWillUnmount() {
document.body.removeEventListener('click');
}
handleBtnClick = (e) => {
const { visible } = this.state;
this.setState({
visible: !visible,
});
}
render() {
const { visible } = this.state;
return (
<div>
<div
className="dialog"
style={{
display: visible ? 'block' : 'none',
position: 'fixed',
top: 100,
left: '50%',
marginLeft: -190,
width: 380,
height: 300,
background: '#fff',
zIndex: 999
}}
>
喵喵~~~
</div>
<Button onClick={this.handleBtnClick} className="btn">{visible ? 'close' : 'open'}</Button>
</div>
);
}
}
复制代码
二、仅使用原生事件dom
class Demo extends PureComponent {
state = {
visible: false,
};
componentDidMount() {
document.body.addEventListener('click', (e) => {
if (e.target && e.target.matches('.dialog')) {
return;
}
this.setState({
visible: false,
});
});
document.querySelector('.btn').addEventListener('click', (e) => {
e.preventDefault();
e.cancelBubble = true;
const { visible } = this.state;
this.setState({
visible: !visible,
});
});
}
componentWillUnmount() {
document.body.removeEventListener('click');
document.querySelector('.dialog').removeEventListener('click');
}
render() {
const { visible } = this.state;
return (
<div>
<div
className="dialog"
style={{
display: visible ? 'block' : 'none',
position: 'fixed',
top: 100,
left: '50%',
marginLeft: -190,
width: 380,
height: 300,
background: '#fff',
zIndex: 999
}}
>
喵喵~~~
</div>
<Button className="btn">{visible ? 'close' : 'open'}</Button>
</div>
);
}
}
复制代码
看到这里,是否是有发现点什么了?函数
React 基于 Virtual Dom 实现了一个事件合成的机制,咱们所注册的事件,会合成一个 SyntheticEvent 对象,若是想访问原生的事件对象,能够访问 nativeEvent 属性。React 事件机制,消除了浏览器的兼容性问题,而且保持与原生事件一致的表现。源码分析
packages/react-dom/src/events/ReactBrowserEventEmitter.js
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginHub` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginHub` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
复制代码
按照流程图的顺序浏览下事件机制的实现
一切故事从这里开始...
packages/react-dom/src/client/ReactDOMComponent.js
ReactDOMComponent 会遍历 ReactNode 的 props 对象,设置待渲染的真实 DOM 对象的一系列的属性,也包括事件注册。
// function diffProperties
if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// 还没有委托事件时异常
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
// 处理事件类型的 props
ensureListeningTo(rootContainerElement, propKey);
}
// ...
}
复制代码
事件委托,全部的事件最终都会被委托到 document 或者 fragment上去
function ensureListeningTo(
rootContainerElement: Element | Node,
registrationName: string, // registrationName:传过来的 onClick
): void {
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE
|| rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
// 取出 element 所在的 document
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}
复制代码
继续看 listenTo 的代码
export function listenTo(
registrationName: string,
mountAt: Document | Element | Node,
): void {
const listeningSet = getListeningSetForElement(mountAt);
// registrationNameDependencies 存储了 React 事件名与浏览器原生事件名对应的一个 Map
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
// 调用该方法进行注册
listenToTopLevel(dependency, mountAt, listeningSet);
}
}
复制代码
listenToTopLevel 方法
export function listenToTopLevel(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listeningSet: Set<DOMTopLevelEventType | string>,
): void {
if (!listeningSet.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL:
// trapCapturedEvent 捕获事件
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.
listeningSet.add(TOP_BLUR);
listeningSet.add(TOP_FOCUS);
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// 在目标 DOM 元素上监听,会冒泡的直接跳过
break;
default:
// 默认状况,在顶层监听全部非媒体事件,媒体事件不会冒泡,所以添加侦听器不会作任何事情
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
if (!isMediaEvent) {
// trapBubbledEvent 冒泡
trapBubbledEvent(topLevelType, mountAt);
}
break;
}
listeningSet.add(topLevelType);
}
}
复制代码
捕获事件 && 事件冒泡
// 捕获事件
export function trapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element | Node,
): void {
trapEventForPluginEventSystem(element, topLevelType, true);
}
// 事件冒泡
export function trapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element | Node,
): void {
trapEventForPluginEventSystem(element, topLevelType, false);
}
function trapEventForPluginEventSystem(
element: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
capture: boolean, // capture true 捕获, false 冒泡
): void {
// ...
if (capture) {
// 捕获事件
addEventCaptureListener(element, rawEventName, listener);
} else {
// 冒泡
addEventBubbleListener(element, rawEventName, listener);
}
}
export function addEventCaptureListener(
element: Document | Element | Node,
eventType: string,
listener: Function,
): void {
element.addEventListener(eventType, listener, true);
}
复制代码
事件注册上了,那而后呢?
继续看 EventPluginHub,它负责管理和注册各类插件。React 事件系统使用了插件机制来管理不一样行为的事件,这些插件会处理对应类型的事件,并生成合成事件对象。
在 ReactDOM 启动时就会向 EventPluginHub 注册如下插件
// packages/react-dom/src/client/ReactDOMClientInjection.js
EventPluginHubInjection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
复制代码
一、packages/react-dom/src/events/ChangeEventPlugin.js
change事件是React的一个自定义事件,旨在规范化表单元素的变更事件。 它支持这些表单元素: input, textarea, select
二、packages/react-dom/src/events/EnterLeaveEventPlugin.js
mouseEnter mouseLeave 和 pointerEnter pointerLeave 这两类比较特殊的事件
三、packages/react-dom/src/events/SelectEventPlugin.js
和 change 事件同样,React 为表单元素规范化了 select (选择范围变更)事件,适用于 input、textarea、contentEditable 元素.
四、packages/react-dom/src/events/SimpleEventPlugin.js
简单事件, 处理一些比较通用的事件类型
五、packages/react-dom/src/events/BeforeInputEventPlugin.js
beforeinput 事件
分析下 SimpleEventPlugin
/**
* Turns
* ['abort', ...]
* into
* eventTypes = {
* 'abort': {
* phasedRegistrationNames: {
* bubbled: 'onAbort',
* captured: 'onAbortCapture',
* },
* dependencies: [TOP_ABORT],
* },
* ...
* };
* topLevelEventsToDispatchConfig = new Map([
* [TOP_ABORT, { sameConfig }],
* ]);
*/
复制代码
// 生成一个合成事件,每一个 plugin 都有这个函数
extractEvents: function(
topLevelType: TopLevelType,
eventSystemFlags: EventSystemFlags,
targetInst: null | Fiber,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
}
// ...
// 从对象池中取出这个 event 的一个实例
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
accumulateTwoPhaseDispatches(event);
return event;
}
复制代码
EventPropagators
// packages/legacy-events/EventPropagators.js
// 这个函数的做用是给合成事件加上 listener,最终全部同类型的 listener 都会放到 _dispatchListeners 里
function accumulateDirectionalDispatches(inst, phase, event) {
if (__DEV__) {
warningWithoutStack(inst, 'Dispatching inst must not be null');
}
// 根据事件阶段的不一样取出响应的事件
const listener = listenerAtPhase(inst, event, phase);
if (listener) {
// 这里将全部的 listener 都存入 _dispatchListeners 中
// _dispatchListeners = [onClick, outClick]
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
// 找到不一样阶段(捕获/冒泡)元素绑定的回调函数 listener
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
复制代码
// packages/legacy-events/EventPluginHub.js
/**
* @param {object} inst The instance, which is the source of events.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} The stored callback.
*/
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);
if (!props) {
// Work in progress.
return null;
}
listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
invariant();
return listener;
}
复制代码
总结:合成事件收集了一波同类型例如 click 的回调函数存在了 event._dispatchListeners 里
注册到 document 上的事件,对应的回调函数都会触发 dispatchEvent 方法,它是事件分发的入口方法。
export function dispatchEvent(
topLevelType: DOMTopLevelEventType, // 带 top 的事件名,如 topClick。
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent, // 用户触发 click 等事件时,浏览器传递的原生事件
): void {
if (!_enabled) {
return;
}
if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) {
// 已经有一个事件队列,这是另一个事件
// 事件须要按顺序分发.
queueDiscreteEvent(
null,
topLevelType,
eventSystemFlags,
nativeEvent,
);
return;
}
const blockedOn = attemptToDispatchEvent(
topLevelType,
eventSystemFlags,
nativeEvent,
);
if (blockedOn === null) {
// We successfully dispatched this event.
clearIfContinuousEvent(topLevelType, nativeEvent);
return;
}
if (isReplayableDiscreteEvent(topLevelType)) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, nativeEvent);
return;
}
if (
queueIfContinuousEvent(
blockedOn,
topLevelType,
eventSystemFlags,
nativeEvent,
)
) {
return;
}
// 由于排队是累积性的,因此只有在不排队时才须要清除
clearIfContinuousEvent(topLevelType, nativeEvent);
// in case the event system needs to trace it.
if (enableFlareAPI) {
if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
);
}
if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
// React Flare event system
dispatchEventForResponderEventSystem(
(topLevelType: any),
null,
nativeEvent,
getEventTarget(nativeEvent),
eventSystemFlags,
);
}
} else {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
);
}
}
function dispatchEventForPluginEventSystem(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
): void {
const bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
eventSystemFlags,
);
try {
// 容许在同一周期内处理事件队列
// 阻止默认行为 preventDefault
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
复制代码
function dispatchEventForPluginEventSystem(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
): void {
// bookKeeping 用来保存过程当中会使用到的变量的对象。初始化使用了 react 在源码中用到的对象池的方法来避免多余的垃圾回收,
const bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
eventSystemFlags,
);
try {
// 容许在同一周期内处理事件队列
// 阻止默认行为 preventDefault
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
复制代码
事件分发的核心,使用批处理的方式进行事件分发,handleTopLevel 是事件分发的真正执行者。它主要作两件事情,一是利用浏览器回传的原生事件构造出 React 合成事件,二是采用队列的方式处理 events。
function handleTopLevel(bookKeeping: BookKeepingInstance) {
let targetInst = bookKeeping.targetInst;
//遍历层次结构,以防存在任何嵌套的组件。
//重要的是咱们在调用任何祖先以前先创建父数组
//事件处理程序,由于事件处理程序能够修改 DOM,从而致使与 ReactMount 的节点缓存不一致。
let ancestor = targetInst;
// 事件回调函数执行后可能致使 Virtual DOM 结构的变化。
// 执行前,先存储事件触发时的 DOM 结构
do {
if (!ancestor) {
const ancestors = bookKeeping.ancestors;
((ancestors: any): Array<Fiber | null>).push(ancestor);
break;
}
const root = findRootContainerNode(ancestor);
if (!root) {
break;
}
const tag = ancestor.tag;
if (tag === HostComponent || tag === HostText) {
bookKeeping.ancestors.push(ancestor);
}
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
// 依次遍历数组,并执行回调函数,这个顺序就是冒泡的顺序
// 不能经过 stopPropagation 来阻止冒泡。
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
// 事件触发的 DOM
const eventTarget = getEventTarget(bookKeeping.nativeEvent);
const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
// 原生事件 event
const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);
runExtractedPluginEventsInBatch(
topLevelType,
targetInst,
nativeEvent,
eventTarget,
bookKeeping.eventSystemFlags,
);
}
}
复制代码
React 实现了一套冒泡机制,从触发事件的对象开始,向父元素回溯,依次调用它们注册的事件回调函数。
咱们在 React 中定义的事件处理器会接收到一个合成事件对象的示例(使用 nativeEvent 能够访问原生事件对象),React 消除了它在不一样浏览器中的兼容性问题,与原生的浏览器事件同样拥有一样的接口,一样支持冒泡机制,能够试用 stopPropagation() 和 preventDefault() 终端它。除一些媒体事件(例如 onplay onpause),React 并不会把事件直接绑定到真实节点上,而是把事件代理到到 document 上。