需求描述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 上。