某次被問到 React
事件機制的問題,關於這一塊我確實不怎麼清楚,因為平時大部分工作都是用 Vue
,對於 React
的熟悉程度只限於會用,具體實現邏輯還真沒專門學習過,但是總不能就說自己不清楚吧,好在我瞭解 Vue
的事件機制,於是就把 Vue
的事件機制說了一遍,最後再來一句“我覺得 React
應該和 Vue
的差不多”
後來我想了下應該沒那麼簡單,於是網上搜了下相關文章,發現果然是被我想得太簡單了,Vue
通過編譯模板,解析出事件指令,將事件和事件回撥附加到 vnode tree
上,在 patch
過程中的建立階段和更新階段都會對這個 vnode tree
進行處理,拿到每個 vnode
上附加的事件資訊,就可以呼叫原生 DOM API
對相應事件進行註冊或移除,流程還是比較清晰的,而React
則是單獨實現了一套事件機制
本文以
React v16.5.2
為基礎進行原始碼分析
基本流程
在 react
原始碼的 react-dom/src/events/ReactBrowserEventEmitter.js
檔案的開頭,有這麼一大段註釋:
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to ......
* ......
*
* +------------+ .
* | 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
*/
複製程式碼
這段註釋第一段文字內容被我省略掉了,其主要是在大概描述 React
的事件機制,也就是這個檔案中的程式碼要做的一些事情,大概意思就是說事件委託是很常用的一種瀏覽器事件優化策略,於是 React
就接管了這件事情,並且還貼心地消除了瀏覽器間的差異,賦予開發者跨瀏覽器的開發體驗,主要是使用 EventPluginHub
這個東西來負責排程事件的儲存,合成事件並以物件池的方式實現建立和銷燬,至於下面的結構圖形,則是對事件機制的一個圖形化描述
根據這段註釋,大概可以提煉出以下幾點內容:
React
事件使用了事件委託的機制,一般事件委託的作用都是為了減少頁面的註冊事件數量,減少記憶體開銷,優化瀏覽器效能,React
這麼做也是有這麼一個目的,除此之外,也是為了能夠更好的管理事件,實際上,React
中所有的事件最後都是被委託到了document
這個頂級DOM
上- 既然所有的事件都被委託到了
document
上,那麼肯定有一套管理機制,所有的事件都是以一種先進先出的佇列方式進行觸發與回撥 - 既然都已經接管事件了,那麼不對事件做些額外的事情未免有些浪費,於是
React
中就存在了自己的 合成事件(SyntheticEvent
),合成事件由對應的EventPlugin
負責合成,不同型別的事件由不同的plugin
合成,例如SimpleEvent Plugin
、TapEvent Plugin
等 - 為了進一步提升事件的效能,使用了
EventPluginHub
這個東西來負責合成事件物件的建立和銷燬
下文均以下述這段程式碼為示例進行分析:
export default class MyBox extends React.Component {
clickHandler(e) {
console.log('click callback', e)
}
render() {
return (
<div className="box" onClick={this.clickHandler}>文字內容</div>
)
}
}
複製程式碼
事件註冊
只看相關主體流程,其他諸如 vnode
的建立等前提流程就不管了,從setInitialDOMProperties
這個方法開始看起,這個方法主要用於遍歷 ReactNode
的 props
物件,給最後將要真正渲染的真實 DOM
物件設定一系列的屬性,例如 style
、class
、autoFocus
,也包括innerHTML
、event
的處理等,示例中 .box
元素的 props
物件結構如下:
這個方法中有個 case
,就是專門用於處理事件的:
// react-dom/src/client/ReactDOMComponent.js
else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (true && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
// 處理事件型別的 props
ensureListeningTo(rootContainerElement, propKey);
}
}
複製程式碼
其中的 registrationNameModules
這個變數,裡面存在一大堆的屬性,都是與 React
的事件相關:
例子中的 onClick
這個 props
顯然符合,所以可以執行 ensureListeningTo
這個方法:
// react-dom/src/client/ReactDOMComponent.js
function ensureListeningTo(rootContainerElement, registrationName) {
var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}
複製程式碼
這個方法中,首先判斷了 rootContainerElement
是不是一個 document
或者 Fragment
(文件片段節點),示例中傳過來的是 .box
這個 div
,顯然不是,所以 doc
這個變數就被賦值為 rootContainerElement.ownerDocument
,這個東西其實就是 .box
所在的 document
元素,把這個document
傳到下面的 listenTo
裡了,事件委託也就是在這裡做的,所有的事件最終都會被委託到 document
或者 fragment
上去,大部分情況下都是 document
,然後這個 registrationName
就是事件名稱 onClick
接著開始執行 listenTo
方法,這個方法其實就是註冊事件的入口了,方法裡面有這麼一句:
// react-dom/src/events/ReactBrowserEventEmitter.js
var dependencies = registrationNameDependencies[registrationName];
複製程式碼
registrationName
就是傳過來的 onClick
,而變數 registrationNameDependencies
是一個儲存了 React
事件名與瀏覽器原生事件名對應的一個 Map
,可以通過這個 map
拿到相應的瀏覽器原生事件名,registrationNameDependencies
結構如下:
可以看到,React
是給事件名做了一些跨瀏覽器相容事情的,比如傳入 onChange
事件,會自動對應上 blur change click focus
等多種瀏覽器原生事件
接下來,遍歷這個 dependencies
陣列,進入到以下 case
:
// react-dom/src/events/ReactBrowserEventEmitter.js
switch (dependency) {
// 省略一些程式碼
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.
var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt);
}
break;
}
複製程式碼
除了 scroll focus blur cancel close
方法走 trapCapturedEvent
方法,invalid submit reset
方法不處理之外,剩下的事件型別全走default
,執行 trapBubbledEvent
這個方法,trapCapturedEvent
和 trapBubbledEvent
二者唯一的不同之處就在於,對於最終的合成事件,前者註冊捕獲階段的事件監聽器,而後者則註冊冒泡階段的事件監聽器
由於大部分合成事件的代理註冊的都是冒泡階段的事件監聽器,也就是委託到
document
上註冊的是冒泡階段的事件監聽器,所以就算你顯示宣告瞭一個捕獲階段的React
事件,例如onClickCapture
,此事件的響應也會晚於原生事件的捕獲事件以及冒泡事件 實際上,所有原生事件的響應(無論是冒泡事件還是捕獲事件),都將早於React
合成事件(SyntheticEvent
),對原生事件呼叫e.stopPropagation()
將阻止對應SyntheticEvent
的響應,因為對應的事件根本無法到達document
這個事件委託層就被阻止掉了
二者區別不大,trapBubbledEvent
用的最多,本示例也將執行這個方法,所以就跟著這個方法看下去:
// react-dom/src/events/EventListener.js
// 對於本示例來說,topLevelType就是 click,element就是 document
function trapBubbledEvent(topLevelType, element) {
if (!element) {
return null;
}
var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;
addEventBubbleListener(element, getRawEventName(topLevelType),
// Check if interactive and wrap in interactiveUpdates
dispatch.bind(null, topLevelType));
}
複製程式碼
addEventBubbleListener
這個方法接收三個引數,在本示例中,第一個引數 element
其實就是 document
元素,getRawEventName(topLevelType)
就是 click
事件,第三個引數的 dispatch
就是 dispatchInteractiveEvent
,dispatchInteractiveEvent
其實最後還是會執行 dispatchEvent
這個方法,只是在執行這個方法之前做了一些額外的事情,這裡不需要關心,可以暫且認為二者是一樣的
看下 addEventBubbleListener
這個方法:
// react-dom/src/events/EventListener.js
export function addEventBubbleListener(
element: Document | Element,
eventType: string,
listener: Function,
): void {
element.addEventListener(eventType, listener, false);
}
複製程式碼
這個方法很簡單,就是用 addEventListener
給 document
註冊了一個冒泡事件,listener
這個事件的回撥就是之前傳入 dispatch.bind(null, topLevelType)
流程圖如下:
事件分發
既然所有的事件都委託註冊到了 document
上,那麼事件觸發的時候,肯定需要一個事件分發的過程,來找到到底是哪個元素觸發的事件,並執行相應的回撥函式,需要注意的是,由於元素本身並沒有註冊任何事件,而是委託到了 document
上,所以這個將被觸發的事件是 React
自帶的合成事件,而非瀏覽器原生事件,但總之都是需要一個分發的過程的
在前面的 事件註冊 中已經提到過,註冊到 document
上的事件,對應的回撥函式都會觸發 dispatchEvent
這個方法,進入這個方法:
// react-dom/src/events/ReactDOMEventListener.js
const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
複製程式碼
首先找到事件觸發的 DOM
和 React Component
,找真實 DOM
比較好找,直接取事件回撥的 event
引數的 target | srcElement | window
即可,然後這個 nativeEventTarget
物件上掛在了一個以 __reactInternalInstance
開頭的屬性,這個屬性就是 internalInstanceKey
,其值就是當前 React
例項對應的 React Component
然後繼續往下看:
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
batchedUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
複製程式碼
batchedUpdates
,字面意思就是批處理更新,這裡實際上就是把當前觸發的事件放入了批處理佇列中,其中,handleTopLevel
是事件分發的核心所在:
// react-dom/src/events/ReactDOMEventListener.js
let targetInst = bookKeeping.targetInst;
// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
let ancestor = targetInst;
do {
if (!ancestor) {
bookKeeping.ancestors.push(ancestor);
break;
}
const root = findRootContainerNode(ancestor);
if (!root) {
break;
}
bookKeeping.ancestors.push(ancestor);
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
複製程式碼
首先在事件回撥之前,根據當前元件,向上遍歷得到其所有的父元件,儲存到 ancestors
中,由於所有的事件都委託到了 document
上,所以在事件觸發後,無論是冒泡事件還是捕獲事件,其在相關元素上的觸發肯定是要有一個次序關係的,比如在子元素和父元素上都註冊了一個滑鼠點選冒泡事件,事件觸發後,肯定是子元素的事件響應快於父元素,所以在事件佇列裡,子元素就要排在父元素前面,而在事件回撥之前就要進行快取,原因在程式碼的註釋裡也已經解釋得很清楚了,大概意思就是事件回撥可能會改變 DOM
結構,所以要先遍歷好元件層級關係,快取起來
繼續往下:
// react-dom/src/events/ReactDOMEventListener.js
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
runExtractedEventsInBatch(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent),
);
}
複製程式碼
使用了一個 for
迴圈來遍歷這個 React Component
及其所有的父元件,執行 runExtractedEventsInBatch
方法,這裡的遍歷方法是從前往後遍歷,前面說了,我們這裡分析的是 trapBubbledEvent
,也就是冒泡事件,所以這裡對應到元件層級上就是由子元素到父元素,如果這裡是分析 trapCapturedEvent
,即捕獲事件,那麼這個從前往後的順序就對應父元素到子元素了
提醒一點,無論是 trapBubbledEvent
還是 trapCapturedEvent
,這裡都是針對 document
元素而不是實際的元素,不要弄混了
至於迴圈中呼叫的 runExtractedEventsInBatch
方法,其實就是事件執行的入口了
事件執行
runExtractedEventsInBatch
這個方法中又呼叫了兩個方法:extractEvents
、runEventsInBatch
,extractEvents
用於構造合成事件,runEventsInBatch
用於批處理 extractEvents
構造出的合成事件
構造合成事件
找到合適的合成事件的 plugin
先看 extractEvents
// packages/events/EventPluginHub.js
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) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
複製程式碼
首先遍歷 plugins
,這個 plugins
就是所有事件合成 plugins
的集合陣列,一共 5
種(v15.x
版本是 7
種),這些 plugins
都位於 react-dom/src/events
這個資料夾下,以單獨檔案的形式存在,檔名以 EventPlugin
結尾的就是,它們是在 EventPluginHub
初始化階段注入進去的:
// react-dom/src/client/ReactDOMClientInjection.js
EventPluginHub.injection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
複製程式碼
extractEvents
方法裡用了一個 for
迴圈,把所有的 plugin
全都執行了一遍,個人理解沒這個必要,找到合適的 plugin
執行完之後就可以直接 break
掉了
比如對於本示例的 click
事件來說,合適的 plugin
是 SimpleEventPlugin
,其他的 plugin
就算是進入走了一遍也只是做了個無用功而已,因為執行完其他 plugin
後得到的 extractedEvents
都不滿足 if (extractedEvents)
這個條件,無法給 events
這個變數賦值或者覆蓋賦值,當然,也可能這段程式碼還有其他比較隱祕的作用吧
possiblePlugin.extractEvents
這一句就是呼叫相應 plugin
的構造合成事件的方法,其他的 plugin
就不展開分析了,針對本示例的 SimpleEventPlugin
,來看下它的 extractEvents
:
// react-dom/src/events/SimpleEventPlugin.js
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
}
複製程式碼
首先,看下 topLevelEventsToDispatchConfig
這個物件中有沒有 topLevelType
這個屬性,只要有,那麼說明當前事件可以使用 SimpleEventPlugin
構造合成事件,對於本示例來說,topLevelType
就是 click
,而topLevelEventsToDispatchConfig
結構如下:
這些屬性就是一些常見的事件名,顯然 click
是 topLevelEventsToDispatchConfig
的一個屬性名,符合條件,可以繼續往下執行,下面緊跟著的是一個 switch...case
的判斷語句,對於本示例來說,將在下面這個 case
處 break
掉:
// react-dom/src/events/SimpleEventPlugin.js
case TOP_CLICK:
// 省略了一些程式碼
EventConstructor = SyntheticMouseEvent;
break;
複製程式碼
SyntheticMouseEvent
可以看做是 SimpleEventPlugin
的一個具體的子 plugin
,相當於是對 SimpleEventPlugin
這個大概念的 plugin
又細分了一層,除了 SyntheticMouseEvent
之外還有 SyntheticWheelEvent
、SyntheticClipboardEvent
、SyntheticTouchEvent
等
從合成事件物件池中取物件
設定好具體的 EventConstructor
後,繼續往下執行:
// react-dom/src/events/SimpleEventPlugin.js
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
accumulateTwoPhaseDispatches(event);
return event;
複製程式碼
getPooled
就是從 event
物件池中取出合成事件,這種操作是 React
的一大亮點,將所有的事件快取在物件池中,可以大大降低物件建立和銷燬的時間,提升效能
getPooled
是 EventConstructor
上的一個方法,這個方法是在 EventConstructor
初始化的時候掛上去的,但歸根到底,這個方法是位於 SyntheticEvent
這個物件上,流程示意圖如下:
這個 getPooled
其實就是 getPooledEvent
,在 SyntheticEvent
初始化的過程中就被設定好初始值了:
// packages/events/SyntheticEvent.js
addEventPoolingTo(SyntheticEvent);
// 省略部分程式碼
function addEventPoolingTo(EventConstructor) {
EventConstructor.eventPool = [];
EventConstructor.getPooled = getPooledEvent;
EventConstructor.release = releasePooledEvent;
}
複製程式碼
那麼看下 getPooledEvent
:
// packages/events/SyntheticEvent.js
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
const EventConstructor = this;
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop();
EventConstructor.call(
instance,
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
return instance;
}
return new EventConstructor(
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
}
複製程式碼
首次觸發事件的時候(在本示例中就是 click
事件),EventConstructor.eventPool.length
為 0
,因為這個時候是第一次事件觸發,物件池中沒有對應的合成事件引用,所以需要初始化,後續再觸發事件的時候,就無需 new
了,而是走上面那個邏輯,直接從物件池中取,通過 EventConstructor.eventPool.pop();
獲取合成物件例項
這裡先看下初始化的流程,會執行 new EventConstructor
這一句,前面說了,這個東西可以看做是 SyntheticEvent
的子類,或者是由 SyntheticEvent
擴充套件而來的東西,怎麼擴充套件的呢,實際上是使用了一個 extend
方法:
const SyntheticMouseEvent = SyntheticUIEvent.extend({
screenX: null,
screenY: null,
clientX: null,
clientY: null,
pageX: null,
pageY: null,
// 省略部分程式碼
})
複製程式碼
首先,SyntheticMouseEvent
這個合成事件,有自己的一些屬性,這些屬性其實和瀏覽器原生的事件回撥引數物件 event
的屬性沒多大差別,都有對於當前事件的一些描述,甚至連屬性名都一樣,只不過相比於瀏覽器原生的事件回撥引數物件 event
來說,SyntheticMouseEvent
或者說 合成事件SyntheticEvent
的屬性是由 React
主動生成,經過 React
的內部處理,使得其上附加的描述屬性完全符合 W3C
的標準,因此在事件層面上具有跨瀏覽器相容性,與原生的瀏覽器事件一樣擁有同樣的介面,也具備stopPropagation()
和 preventDefault()
等方法
對於本示例中的點選事件回撥方法來說:
clickHandler(e) {
console.log('click callback', e)
}
複製程式碼
其中的 e
其實就是 合成事件而非瀏覽器原生事件的 event
,所以開發者無需考慮瀏覽器相容性,只需要按照 w3c
規範取值即可,如果需要訪問原生的事件物件,可以通過 e.nativeEvent
獲得
SyntheticUIEvent
這個東西主要就是往 SyntheticMouseEvent
上加一些額外的屬性,這裡不用關心,然後這個 SyntheticMouseEvent.extend
又是由 SyntheticEvent
擴充套件 (extend
)來的,所以最終會 new SyntheticEvent
先看下 extend
方法:
// packages/events/SyntheticEvent.js
SyntheticEvent.extend = function(Interface) {
const Super = this;
// 原型式繼承
const E = function() {};
E.prototype = Super.prototype;
const prototype = new E();
// 建構函式繼承
function Class() {
return Super.apply(this, arguments);
}
Object.assign(prototype, Class.prototype);
Class.prototype = prototype;
Class.prototype.constructor = Class;
Class.Interface = Object.assign({}, Super.Interface, Interface);
Class.extend = Super.extend;
addEventPoolingTo(Class);
return Class;
};
複製程式碼
先來了個經典的寄生組合式繼承,這種寄生方法最為成熟,大多數庫都是使用這種繼承方法,React
這裡也用了它,讓EventConstructor
繼承於 SyntheticEvent
,獲得 SyntheticEvent
上的一些屬性和方法,如前面所說的 eventPool
、getPooled
等
既然存在繼承關係,那麼 new EventConstructor
這個子類,自然就會呼叫父類 SyntheticEvent
的new
方法,也就是開始呼叫合成元件的構造器了,開始真正構造合成事件,主要就是將原生瀏覽器事件上的引數掛載到合成事件上,包括 clientX
、screenY
、timeStamp
等事件屬性, preventDefault
、stopPropagation
等事件方法,例如前面所說的通過 e.nativeEvent
獲得的原生事件就是在這個時候掛載上去的:
// packages/events/SyntheticEvent.js
this.nativeEvent = nativeEvent;
複製程式碼
掛載的屬性都是經過 React
處理過的,具備跨瀏覽器能力,同樣,掛載的方法也和原生瀏覽器的事件方法有所不同,因為此時的事件附加在 document
上的,所以呼叫一些事件方法,例如 e.stopPropagation()
其實是針對 document
元素呼叫的,跟原本期望的元素不是同一個,那麼為了讓合成事件的表現達到原生事件的效果,就需要對這些方法進行額外的處理
處理的方法也比較簡單,就是加了一個標誌位,例如,對於 stopPropagation
來說, React
對其進行了包裝:
// packages/events/SyntheticEvent.js
stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation();
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
}
複製程式碼
首先就是拿到瀏覽器原生事件,然後呼叫對應的 stopPropagation
方法,這裡需要注意一下,這裡的 event
是由 document
這個元素上的事件觸發而生成的事件回撥的引數物件,而非實際元素的事件回撥的引數物件,說得明白點,就是給document
上觸發的事件,例如點選事件,呼叫了一下 e.stopPropagation
,阻止事件繼續往 document
或者 Fragment
的父級傳播
// packages/events/SyntheticEvent.js
// 這個函式其實就是返回了一個 true,與此對應的,還有個函式名為 functionThatReturnsFalse的函式,用來返回 false
function functionThatReturnsTrue() {
return true;
}
複製程式碼
關鍵在於 this.isPropagationStopped = functionThatReturnsTrue;
這一句,相當於是設定了一個標誌位,對於冒泡事件來說,當事件觸發,由子元素往父元素逐級向上遍歷,會按順序執行每層元素對應的事件回撥,但如果發現當前元素對應的合成事件上的 isPropagationStopped
為 true
值,則遍歷的迴圈將中斷,也就是不再繼續往上遍歷,當前元素的所有父元素的合成事件就不會被觸發,最終的效果,就和瀏覽器原生事件呼叫 e.stopPropagation()
的效果是一樣的
捕獲事件的原理與此相同,只不過是由父級往子級遍歷的罷了
這些事件方法(包括 stopPropagation
、preventDefault
等)一般都是在事件回撥函式內呼叫,而事件的回撥函式則是在後面的批處理操作中執行的
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
複製程式碼
拿到所有與當前觸發事件相關的元素例項和事件回撥函式
上述一大堆都是從上述程式碼的第一句 getPooled
為入口進去的,主要是為了得到合成事件,拿到基本的合成事件以後,開始對這個合成事件進行進一步的加工,也就是 accumulateTwoPhaseDispatches
這個方法要做的事情,這個方法涉及到的流程比較多,畫個圖清晰點:
程式碼和呼叫的方法都比較瑣碎,但目標很清晰,就是儲存當前元素及其父元素上掛在的所有事件回撥函式,包括捕獲事件(captured
)和冒泡事件(bubbled
),儲存到事件event
的 _dispatchListeners
屬性上,並且將當前元素及其父元素的react
例項(在 v16.x
版本中,這裡的例項是一個 FiberNode
)儲存到event
的 _dispatchInstances
屬性上
拿到了所有與事件相關的元素例項以及事件的回撥函式之後,就可以對合成事件進行批量處理了
由於
React
的事件機制比較複雜,要說的地方有點多,所以分為了兩篇文章,剩餘分析部分請參見文章 React事件機制 - 原始碼概覽(下)