
壹 ❀ 引
在前面兩篇文章中,我們花了較大的篇幅介紹react
的setState
方法,在介紹setState
同步異步時提到,在react
合成事件中react
對於this.state
更新都是異步,但在原生事件中更新卻是同步,這說明react
在合成事件處理上必定與原生事件存在部分差異,那么本篇文章就來着重介紹react
中的合成事件,在文章開始之前我們先羅列部分問題:
- 了解原生事件監聽機制嗎?為什么需要事件代理?
- react如何實現的合成事件?這么做的好處是什么?
- 合成事件與原生事件執行先后順序。
- 合成事件阻止冒泡會阻塞原生事件嗎?原生事件阻止冒泡會阻塞合成事件嗎?
在文章開始前,大家可以先自行思考這些問題,假設這些在面試中遇到,你能回答多少呢?那么本文開始。
貳 ❀ 從原生事件說起
雖然本文的核心重點是介紹react
的合成事件,但文章開頭既然給了合成事件與原生事件相關對比問題,因此了解原生事件是有必要的,考慮到可能有同學對此類知識存在遺忘,這里做個簡單復習。
首先讓我們復習下事件監聽api addEventListener
,基本語法如下:
element.addEventListener(event, function, useCapture);
其中element
表示你需要監聽的dom
元素;event
表示監聽的事件類型,比如onclick,onblur
;function
表示觸發事件后的回調,你需要做什么都可以寫在這里;useCapture
是一個布爾值,表示是否開啟捕獲階段,默認false
。
以如下代碼結構為例,當我們點擊span
時,必然會經歷過捕獲階段----》目標階段----》冒泡階段
:

我們用一個例子復習這個過程:
<div id="div">
我是div
<p id="p">
我是p
<span id="span">我是span</span>
</p>
</div>
const div = document.querySelector("#div");
const p = document.querySelector("#p");
const span = document.querySelector("#span");
// 捕獲階段,這里將useCapture設置為true
div.addEventListener("click",()=>console.log("捕獲階段--div"),true);
p.addEventListener("click",()=>console.log("捕獲階段--p"),true);
// 目標階段
span.addEventListener("click",()=>console.log("目標階段--span"));
// 冒泡階段,useCapture默認false,不寫了
div.addEventListener("click",()=>console.log("捕獲階段--div"));
p.addEventListener("click",()=>console.log("捕獲階段--p"));

既然提到了事件監聽,那么有三個API
就不得不提了,它們分別是event.preventDefault
、event.stopPropagation
與event.stopImmediatePropagation
。讓我們先聊聊stopPropagation
,此方法一般用於阻止冒泡,比如父子都綁定了點擊事件,但點擊子時我不希望父在冒泡階段也被觸發,因此通過在子的事件回調中添加此方法能做到這一點,修改上述例子中目標階段的代碼為:
span.addEventListener("click", (e) => {
e.stopPropagation();
console.log("目標階段--span")
});
此時點擊span
會發現只會輸出span
,而冒泡階段的div
與p
都被阻止執行了。

關於event.preventDefault
,此方法常用於阻止元素默認行為,比如點擊a
標簽除了執行我們綁定的click
事件外,它還會執行a
標簽默認的跳轉。再或者form
表達點擊提交會將form
的值傳遞給action
指定地址並刷新頁面,像這類行為我們均可以通過preventDefault
阻止。
在介紹stopImmediatePropagation
之前,我們需要知道事件監聽相對於普通事件綁定的一大好處是,事件監聽支持為同一dom
監聽多個行為,但如果是普通的事件綁定后者會覆蓋前者:
span.onclick = ()=>console.log('事件綁定-1');
// 后綁定的事件會覆蓋前面的綁定
span.onclick = ()=>console.log('事件綁定-2');
// 事件監聽就不會存在覆蓋,下面2個都會執行
span.addEventListener("click", (e) => {
console.log("事件監聽-1")
});
span.addEventListener("click", (e) => {
console.log("事件監聽-2")
});

那既然事件監聽支持為同一dom
綁定多個,我在執行了某個監聽后,需要將其它監聽都阻止掉怎么辦?此時就輪到stopImmediatePropagation
出場立大功了,看個例子:
// 捕獲階段
div.addEventListener("click", () => console.log("捕獲階段--div-1"), true);
div.addEventListener("click", () => console.log("捕獲階段--div-2"), true);
p.addEventListener("click", () => console.log("捕獲階段--p-1"), true);
p.addEventListener("click", () => console.log("捕獲階段--p-2"), true);
// 目標階段
span.addEventListener("click", (e) => {
e.stopImmediatePropagation();
console.log("目標階段---span-1")
});
span.addEventListener("click", (e) => {
console.log("目標階段---span-2")
});
// 冒泡階段
div.addEventListener("click", () => console.log("捕獲階段--div-1"));
div.addEventListener("click", () => console.log("捕獲階段--div-2"));
p.addEventListener("click", () => console.log("捕獲階段--p-1"));
p.addEventListener("click", () => console.log("捕獲階段--p-2"));

可以看到stopImmediatePropagation
同樣會阻止事件冒泡,但除此之外,它還會阻止同一dom
身上的其它事件執行。
那么聊完事件監聽,什么是事件代理?在現實生活中,我們網購到公司的快遞大部分都會由前台代簽收,而不是分別送到我們每個人手上,此時前台就相當於做了一個代理的事情,原本需要不同的多個人分別簽收的行為,統一與前台代理處理。
映射到代碼中,假設有ul>li
的結構,我們希望點擊li
顯示出li
的文本內容,如果給每個li
綁定就得這么寫:
<ul id="ul">
<li onclick="handleClick(event)">1</li>
<li onclick="handleClick(event)">2</li>
<li onclick="handleClick(event)">3</li>
<li onclick="handleClick(event)">4</li>
<li onclick="handleClick(event)">5</li>
</ul>
const handleClick=(e)=>{
console.log(e.target.innerHTML);
};
但如果通過事件代碼,我們將點擊行為委托給li
共同的父元素ul
,代碼將更為清晰簡單:
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
const span = document.getElementById("span");
const handleClick = (e) => {
span.innerHTML =`此時點擊的是第${e.target.innerHTML}個li`;
};
const ul = document.querySelector("#ul");
ul.addEventListener("click", handleClick)

雖然事件被代理給了ul
,但通過event.target
我們還是能拿到實際操作的li
。這讓我想起了17我還在寫JQ的年代,當li
是動態遍歷生成時,如果給li
綁事件你會發現此時li
其實是不存在的,這就導致事件綁定最終失敗,而事件代理在性能更優的前景下更是巧妙解決了這一問題。
OK,關於原生事件監聽與事件代理我們就介紹到這里,這部分知識也利於我們理解react
的合成事件。
叄 ❀ 淺析合成事件原理(16.13.1)
叄 ❀ 壹 綁定階段前置處理
當我們訪問react
官方關於合成事件的文檔,第一能獲取到的信息是react
通過SyntheticEvent
包裝器來統一生成合成事件。需要注意的是react
並不是獨立創造了一套事件系統,所有的合成事件本質上依舊依賴了原生事件;而通過包裝器react
也對原生事件做了normalize
操作,以達到抹平不同瀏覽器之間事件處理差異的目的。
還記得我們在介紹原生事件時提到的事件代理嗎?出於性能優化考慮,react
中的合成事件其實也做了類似處理,絕大多數的合成事件(並不是所有事件)最終都掛載在了document
上,而非你定義組件的真實dom
上,我們先初步了解這個概念,接下來我們通過源碼層面來了解react
合成事件的綁定階段與執行階段。
注意,我在查閱資料的過程中發現,不同react
版本對於合成事件的處理其實是存在差異的,比如react 17
中事件就不再注冊在document
上,而是你的組件所綁定的container
上,我這里的源碼版本我采用的是16.13.1
,另外文中源碼均可在react-dom.development.js
文件中找到。
<button className="button" onClick={this.handleClick}>點擊</button>
在上文我們已經提到,react
合成事件其實依賴了原生事件,那么合成事件類型自然跟原生事件有着一一對應的關系,畢竟react
的點擊事件是駝峰的onClick
,而原生的卻是onclick
,以上述代碼為例,當react
渲染到button
時,發現此組件的props
中有一個合成事件,理論上來說此時react
要做的就是注冊操作,找到對應onClick
的原生事件類型,並做后續包裝動作。
而對於事件類型,其實在react
中提供了一個名為injectEventPluginsByName
的事件分類插件,它會初始化階段自執行注入,通過命名可以發現react
做了不同事件類型的分類:
// 用於copy injectedNamesToPlugins的全局對象
var namesToPlugins = {};
// 這里的injectedNamesToPlugins就是下面自調用注入的不同事件插件對象,我刪除了部分不影響理解的代碼
function injectEventPluginsByName(injectedNamesToPlugins) {
var isOrderingDirty = false;
// 遍歷所有插件對象
for (var pluginName in injectedNamesToPlugins) {
if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
continue;
}
// 按插件key依次獲取value
var pluginModule = injectedNamesToPlugins[pluginName];
if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== pluginModule) {
// 將插件對象按key-value依次賦值給全局對象namesToPlugins
namesToPlugins[pluginName] = pluginModule;
isOrderingDirty = true;
}
}
if (isOrderingDirty) {
recomputePluginOrdering();
}
}
// 初始化階段自執行,注入不同類型的事件插件
injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin
});
出於好奇,我直接在初始化階段斷點,這里就能看到SimpleEventPlugin
每個事件插件類型都包含eventTypes
與extractEvents
兩個對象

其中extractEvents
就是事件最終要執行的函數,而eventTypes
則包含了合成事件對應的原生事件相關信息:

在上述代碼中的recomputePluginOrdering
方法,我們繼續往下跟,能找到下面這個方法:
// 這兩個也是全局對象
var registrationNameModules = {};
var registrationNameDependencies = {};
function publishRegistrationName(registrationName, pluginModule, eventName) {
// 建立合成事件名與事件插件的映射
registrationNameModules[registrationName] = pluginModule;
// 建立合成事件名與原生事件的映射
registrationNameDependencies[registrationName] = pluginModule.eventTypes[eventName].dependencies;
{
var lowerCasedName = registrationName.toLowerCase();
possibleRegistrationNames[lowerCasedName] = registrationName;
if (registrationName === 'onDoubleClick') {
possibleRegistrationNames.ondblclick = registrationName;
}
}
}
這個方法中也做了一件比較重要的事,它也為兩個全局對象做了賦值操作,其中registrationNameModules
用於保存合成事件名與事件插件的映射,比如某個合成事件屬於哪個事件插件,通過斷點我們能看到這個結構:

前文我們已經說過了,每個事件類型對象都包含eventTypes、extractEvents
兩個屬性,所以上圖的結構本質上等同於:
{
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
onChange: ChangeEventPlugin,
onChangeCapture: ChangeEventPlugin,
onMouseEnter: EnterLeaveEventPlugin,
...
}
而registrationNameDependencies
用於保存合成事件與原生事件的映射關系,比如某個合成事件是由哪些原生事件組合模擬的,同樣斷個點:

所以它的結構等同於:
{
onClick: ['click'],
onClickCapture: ['click'],
onClose: ['close'],
onCloseCapture: ['close'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
onChangeCapture: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
}
你會驚奇的發現像合成事件onChange
對應到原生居然有8個原生事件,這說明在react
底層使用了多個原生事件組合模擬一個原生事件,也正因如此,react
才能抹平不同瀏覽器事件差異性,讓同一個合成事件達到相同的交互效果。
上面我們其實省略了很多中間代碼,但總結來說,就是注入合成事件插件,然后對合成事件進行了多次遍歷,跟剝洋蔥似的,遍歷每個事件類型,以及每個事件類型下的每個合成事件,從而得到了多個為后續注冊服務的全局對象。
OK,前置條件說完了,那么一個組件上定義了一個onClick
屬性,react
是如何將它綁定到document
,現在正式介紹綁定階段。
叄 ❀ 貳 綁定階段
當一個組件初始化或者更新階段,react
總是要重新檢查組件身上的props
屬性,看看屬性中有沒有與registrationNameModules
能產生對應的,如果有那說明這個屬性是一個合成事件名,這里以更新組件為例(你想看初始化可以跟setInitialDOMProperties
這個方法):
function diffHydratedProperties(domElement, tag, rawProps, parentNamespace, rootContainerElement) {
switch (tag) {
case 'video':
case 'audio':
// Create listener for each media event
for (var i = 0; i < mediaEventTypes.length; i++) {
// 注意,這里綁定事件傳遞的是dom自身
trapBubbledEvent(mediaEventTypes[i], domElement);
}
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
break;
case 'select':
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ensureListeningTo(rootContainerElement, 'onChange');
break;
}
// 遍歷props
for (var propKey in rawProps) {
if (propKey === CHILDREN) {
// 合成事件與插件映射如果能找到這個propKey,那說明是個合成事件
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// 注冊事件,注意這里傳的是document
ensureListeningTo(rootContainerElement, propKey);
}
} else if (){
// ...
}
}
return updatePayload;
}
這個方法巨長,我刪除了很多多余的代碼,這里提煉下信息,前文我們說絕大多數的事件最終都掛載在document
上,原因其實就是在switch
這里,像video
這類媒體標簽,document
沒辦法模擬它們的事件,因此綁定傳遞的dom
其實是domElement
,就是說你們這類元素的事件太特殊了,我代勞不了,你們還是自己綁自己的。
出於好奇,我跟了下trapBubbledEvent
這個方法,下面貼一下大致過程:
function trapBubbledEvent(topLevelType, element) {
// 調用名為trapEventForPluginEventSystem的方法
trapEventForPluginEventSystem(element, topLevelType, false);
}
// 這個方法跟上面唯一區別就是捕獲為true
function trapCapturedEvent(topLevelType, element) {
trapEventForPluginEventSystem(element, topLevelType, true);
}
function trapEventForPluginEventSystem(container, topLevelType, capture) {
var listener;
// 根據事件類型不同等級,對應生成最終的事件監聽回調
switch (getEventPriorityForPluginSystem(topLevelType)) {
case DiscreteEvent:
listener = dispatchDiscreteEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
break;
case UserBlockingEvent:
listener = dispatchUserBlockingUpdate.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
break;
case ContinuousEvent:
default:
listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
break;
}
// 獲取事件名
var rawEventName = getRawEventName(topLevelType);
// 判斷是不是捕獲階段,分別調用最終的事件監聽方法
if (capture) {
addEventCaptureListener(container, rawEventName, listener);
} else {
addEventBubbleListener(container, rawEventName, listener);
}
}
// 我們看下捕獲與非捕獲階段的實現
function addEventBubbleListener(element, eventType, listener) {
// 可以看到還真是直接綁定在dom上,且捕獲為false
element.addEventListener(eventType, listener, false);
}
function addEventCaptureListener(element, eventType, listener) {
// 同樣綁定在原神自身,但捕獲為true
element.addEventListener(eventType, listener, true);
}
一共三次方法調用:
trapBubbledEvent
與trapCapturedEvent
是一對兄弟,它們為捕獲與非捕獲不同類型事件進行注冊trapEventForPluginEventSystem
是上面兩個方法都會調用的方法,在此方法內部,你會發現react
還會根據事件名類型來定義不同事件等級,最終生成不同級別的事件回調callback
,也就是listener
,但最終它們又根據capture
分別調用了不同方法。- 不管是
addEventBubbleListener
還是addEventCaptureListener
,它們綁定執行都是我們再熟悉不過的addEventListener
方法,只是此時的element
並不是document
而是元素自身。所以你會發現像video
這類標簽的事件綁定對象是也就是這些標簽自己,而非document
!!
OK,代碼繼續往下看,來到上上段代碼中的rawProps
遍歷,這里的registrationNameModules.hasOwnProperty(propKey)
就是檢驗你這個key
是否存在於registrationNameModules(合成事件與事件插件的映射)
中,如果有那說明你一定是個合成事件,然后咱們幫你綁定,緊接着執行:
ensureListeningTo(rootContainerElement, propKey);
注意,特殊的事件前面的switch
已經做過特化處理了,能到這的肯定是平平無奇且document
能代勞的事件。另外,有的同學不理解前面說的document
和container
分別表示誰,咱們斷個點分別演示下:

上圖中我直接在createElement
處斷點,看一眼rootContainerElement
,你會發現所謂的container
其實就是我們創建應用的container
:
ReactDOM.render(element, container[, callback])
那么對應到我的demo
中,其實就是一個id
名為root
的div
:
<div id="root"></div>
那么上文提到的document
又是誰呢?在源碼中有專門獲取document
的代碼:
var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement)
我們可以斷點輸出它,可以看到其實就是html
:

讓我們回到事件綁定函數ensureListeningTo
,看看它做了什么:
// rootContainerElement是容器元素,registrationName是合成事件名
function ensureListeningTo(rootContainerElement, registrationName) {
// 判斷我們的rootContainerElement是不是document或者代碼片段
var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
// 前面已經說過了這里的rootContainerElement是一個普通div,所以一定是false,取rootContainerElement.ownerDocument
var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
// 這里的doc就是document
legacyListenToEvent(registrationName, doc);
}
可能有同學看到傳遞的參數是rootContainerElement
,就在想不是說綁定給document
嗎,怎么傳遞的是container
,其實這個方法就是為了判斷你傳遞的dom
節點是不是document
,是的話直接用,不是的話就取rootContainerElement.ownerDocument
。而上述代碼因為我們已知rootContainerElement
是一個div
容器,因此isDocumentOrFragment
一定是false
,那么doc
取值就自然是rootContainerElement.ownerDocument
,我們同樣可以斷點看看這個屬性輸出什么,結果如下:

其實還是document
......總結來說,ensureListeningTo
方法就是為了確保你的事件最終幫在document
上!
方法最后執行了legacyListenToEvent
,同理看看代碼:
// registrationName合成事件名 mountAt是document
function legacyListenToEvent(registrationName, mountAt) {
// 拿到document上目前已經監聽過的對象
var listenerMap = getListenerMapForElement(mountAt);
// 獲取合成事件名所對應的原生事件數組
var dependencies = registrationNameDependencies[registrationName];
// 遍歷原生對象數組,依次調用legacyListenToTopLevelEvent進行掛載
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
legacyListenToTopLevelEvent(dependency, mountAt, listenerMap);
}
}
這個方法大家看注釋應該就很清楚了,registrationNameDependencies
就是前面我們專門解釋過的合成事件名與對應原生事件的映射,然后遍歷開始進行注冊。OK,我們接着看registrationNameDependencies
代碼:
// topLevelType原生事件名 mountAt此時是document listenerMap ducument此時已經監聽過的對象
function legacyListenToTopLevelEvent(topLevelType, mountAt, listenerMap) {
// 已經監聽過的就不要重復監聽了,沒監聽過的才會執行內部代碼
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
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,
// 這里我刪除了一部分代碼
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(topLevelType) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt);
}
break;
}
listenerMap.set(topLevelType, null);
}
}
legacyListenToTopLevelEvent
方法看着很長,但其實做的事情很簡單,根據已知的listenerMap
判斷當前原生事件之前有沒有被綁定沒,沒綁定那就執行綁定,而方法內部一共就只出現了trapCapturedEvent
與trapBubbledEvent
這兩個綁定事件方法,大家可以直接在本文搜索這兩個方法名,你會發現它兩就是上文已經解釋過的兩個方法,而且最終都走到了element.addEventListener(eventType, listener, true/false)
這一句。
那么到這里,我們完整解釋了事件監聽階段的整個過程,你知道了不同合成事件是如何對應到原生事件,以及最終是怎么樣掛在到document
亦或者元素自身之上的,那么我們緊接着介紹執行階段。
叄 ❀ 叄 執行階段
在說執行階段之前,我們還是得想一想執行階段執行什么,在綁定階段,我們知道最終react
還是會執行如下代碼:
element.addEventListener(eventType, listener, false);
而這里的listener
照理說就應該是事件觸發后執行的callback
,那這個listener
是怎么生成的?它跟我寫在react
代碼中真正的執行回調又是如何關聯的?這就得再次回到上面已經解釋過的trapEventForPluginEventSystem
方法。
在trapEventForPluginEventSystem
方法中我們說會根據事件優先級分別調用dispatchDiscreteEvent
,dispatchUserBlockingUpdate
或者dispatchEvent
來生成listener
,但我在嘗試跟前兩個方法過程中發現,這兩個方法最終都是調用了dispatchEvent
這個方法,以dispatchUserBlockingUpdate
為例:
listener = dispatchUserBlockingUpdate.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
function dispatchUserBlockingUpdate(topLevelType, eventSystemFlags, container, nativeEvent) {
// 本質上還是調用的dispatchEvent.bind()來生成的listener
runWithPriority(UserBlockingPriority, dispatchEvent.bind(null, topLevelType, eventSystemFlags, container, nativeEvent));
}
因此我們只用將目光放到dispatchEvent
上即可,上代碼:
/**
*
* @param {*} topLevelType 原生事件名
* @param {*} eventSystemFlags 一個數字常量1
* @param {*} container 監聽事件的容器
* @param {*} nativeEvent event對象
* @returns
*/
function dispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
// 刪除多余代碼
{
// 最終又調用了一個han'shu
dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, null);
}
}
function dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) {
var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst, eventSystemFlags);
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
我們省略了dispatchEvent
中多余的代碼,發現它最終執行了dispatchEventForLegacyPluginEventSystem
,進一步跟進,此方法一共做了三件事,獲取bookKeeping
對象,調用批量事件更新事件batchedEventUpdates
(本質上又調用handleTopLevel
),以及調用完成后又執行releaseTopLevelCallbackBookKeeping
存儲bookKeeping
方法達到復用目的,無奈繼續看handleTopLevel
實現:
function handleTopLevel(bookKeeping) {
var targetInst = bookKeeping.targetInst;
var ancestor = targetInst;
// 這里一直在while,遍歷保存現有dom結構
do {
if (!ancestor) {
var ancestors = bookKeeping.ancestors;
ancestors.push(ancestor);
break;
}
// 尋找當前節點信息的父節點,往上冒泡
var root = findRootContainerNode(ancestor);
if (!root) {
break;
}
var tag = ancestor.tag;
if (tag === HostComponent || tag === HostText) {
bookKeeping.ancestors.push(ancestor);
}
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
var eventTarget = getEventTarget(bookKeeping.nativeEvent);
var topLevelType = bookKeeping.topLevelType;
var nativeEvent = bookKeeping.nativeEvent;
var eventSystemFlags = bookKeeping.eventSystemFlags; // If this is the first ancestor, we mark it on the system flags
if (i === 0) {
eventSystemFlags |= IS_FIRST_ANCESTOR;
}
// 最終生成合成事件的方法
runExtractedPluginEventsInBatch(topLevelType, targetInst, nativeEvent, eventTarget, eventSystemFlags);
}
}
handleTopLevel
方法的作用其實注釋有解釋,考慮到事件回調可能改變現有的DOM
結構,導致先深度遍歷保存現有的組件層次結構。而從代碼解釋上來看,findRootContainerNode
很明顯就是在找當前節點元素的父元素,如果有父繼續while
循環,這很明顯就是在做一個冒泡操作,緊接着下面的for
循環也正是在根據冒泡的順序依次調用runExtractedPluginEventsInBatch
來生成合成事件。
function runExtractedPluginEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
// 生成合成事件
var events = extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
// 執行事件
runEventsInBatch(events);
}
可能你現在看到extractPluginEvents
已經有點陌生了,但在文章前面我們介紹合成事件名與事件插件映射屬性registrationNameModules
時,有介紹每個對象上都有一個extractEvents
屬性,而這個屬性就是為了將我們代碼中所寫的事件回調,綁定到生成的合成事件上:
function extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
var events = null;
for (var i = 0; i < plugins.length; i++) {
var possiblePlugin = plugins[i];
if (possiblePlugin) {
// 獲取每個事件插件的extractEvents方法,用於生成合成事件
var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
這個方法內有一個全局對象plugins
,這個玩意其實就是一個存放了事件插件對象的數組:
var plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
所以上述其實就在遍歷事件插件,並嘗試生成對應的合成事件,因此我們可以看看extractEvents
的內部實現:
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
var dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
if (!dispatchConfig) {
return null;
}
var EventConstructor;
switch (topLevelType) {
case TOP_KEY_PRESS:
if (getEventCharCode(nativeEvent) === 0) {
return null;
}
case TOP_KEY_DOWN:
case TOP_KEY_UP:
EventConstructor = SyntheticKeyboardEvent;
break;
case TOP_BLUR:
case TOP_FOCUS:
EventConstructor = SyntheticFocusEvent;
break;
default:
EventConstructor = SyntheticEvent;
break;
}
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
return event;
}
代碼中我省略了一部分case
分支情況,但不管哪種情況,都會出現類似SyntheticFocusEvent
以及SyntheticKeyboardEvent
這類方法,稍微看了實現代碼,發現這些構建器其實都是通過SyntheticEvent.extend
繼承而來的子類,而且在代碼最后的switch default
執行,默認也賦予SyntheticEvent
這個構造器。
在拿到構造器后緊接着調用了EventConstructor.getPooled
從事件池中獲取合成事件實例,這也解釋了為什么react
官網一開始就說合成事件是由SyntheticEvent
包裝器生成而來。
我們可以上述代碼中的accumulateTwoPhaseDispatches
繼續往下跟:
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
// 模擬兩個階段的遍歷,捕獲/冒泡事件分派。
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
var i;
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function accumulateDirectionalDispatches(inst, phase, event) {
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
其中traverseTwoPhase
方法至關重要,這個函數的官方注釋也說的尤為清楚,通過正向反向遍歷模擬事件捕獲與事件冒泡階段,而它所執行的fn
其實就是函數accumulateDirectionalDispatches
,這個函數內部的主要職責便是找到節點上事件定義的回調,並將其加入到生成的合成事件event
的_dispatchListeners
屬性中,直到這里,我們走完了合成事件的生成(onClickCapture
與onClick
執行順序差異原來是在合成事件生成階段通過不同方向遍歷來綁定模擬的)以及與合成事件我們定義的callback
建立聯系。
讓我們再次回到runExtractedPluginEventsInBatch
方法,去看一看runEventsInBatch
方法。
function runEventsInBatch(events) {
var processingEventQueue = eventQueue;
eventQueue = null;
// 刪除多余代碼,最終執行
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}
var executeDispatchesAndReleaseTopLevel = function (e) {
return executeDispatchesAndRelease(e);
};
代碼很簡單,然后我的目光就被executeDispatchesAndReleaseTopLevel
這個方法所吸引,直譯過來就是事件執行派發與釋放,因此我們繼續跟進executeDispatchesAndRelease
這個方法:
var executeDispatchesAndRelease = function (event) {
// 如果事件存在,那就按順序執行派發事件
if (event) {
executeDispatchesInOrder(event);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
function executeDispatchesInOrder(event) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
{
validateEventDispatches(event);
}
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
}
}
進一步跟進,終於定位到了executeDispatchesInOrder
方法,而且我們甚至看到了在合成事件生成階段,將事件回調與合成事件與之關聯的event._dispatchListeners
對象,在此方法內部就是按照綁定順序,依次遍歷進行執行。
那么經過長篇大論的代碼跟蹤,我們總算是粗略的跟完了合成事件的生成、綁定與執行三個階段
肆 ❀ 合成事件與原生事件執行先后
在了解完合成事件后,我不禁有一個疑問,如果我給一個dom
同時綁定合成事件與原生事件,到底誰會先執行呢?來看個例子:
class Echo extends Component {
componentDidMount() {
const parentDom = ReactDOM.findDOMNode(this);
const childrenDom = parentDom.querySelector(".button");
childrenDom.addEventListener('click', this.onDomClick, false);
}
onDomClick = (e) => {
console.log('原生事件click');
}
onReactClick = () => {
console.log('合成事件click');
}
render() {
return (
<div>
<button className="button" onClick={this.onReactClick}>點擊</button>
</div>
)
}
}

為什么原生事件比合成事件快呢?通過上面的源碼分析,其實很容易聯想到,在冒泡到document
之前,原生事件已經被觸發,這之后才到了document
開始事件派發,遍歷數組進行react
合成事件callback
的執行,合成事件慢的合情合理。
哎?那如果我們同時給一個dom
綁定原生捕獲事件與合成捕獲事件呢?那按照這個說法,document
在最頂層,那是不是應該合成捕獲事件要早於原生捕獲事件執行呢?來看個例子:
class Echo extends Component {
componentDidMount() {
const parentDom = ReactDOM.findDOMNode(this);
const childrenDom = parentDom.querySelector(".button");
childrenDom.addEventListener('click', this.onDomClick, true);
}
onDomClick = (e) => {
console.log('原生事件捕獲click');
}
onReactClick = () => {
console.log('合成事件捕獲click');
}
render() {
return (
<div>
<button className="button" onClickCapture={this.onReactClick}>點擊</button>
</div>
)
}
}

怎么還是原生事件早於合成事件的捕獲階段?????
在合成事件生成源碼分析中,我們介紹了handleTopLevel
方法提到,合成事件是在當前節點冒泡不斷向上搜集同名的合成事件回調,並且在traverseTwoPhase
這個方法中,通過正向負向兩個遍歷,去模擬的捕獲與冒泡,說直白,根本不存在所謂的合成事件捕獲,其實全都是靠冒泡搜集事件后,控制遍歷順序,來模擬了捕獲與冒泡的事件執行順序!!!
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
var i;
// 捕獲倒序遍歷
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
// 冒泡正向遍歷
fn(path[i], 'bubbled', arg);
}
}
因此合成事件的捕獲,說到底還是在原生事件冒泡之后,因為我不冒泡事件你都沒搜集其,捕獲個啥呢?
所以總結來說,合成事件不管捕獲還是冒泡都晚於原生事件,結合之前的源碼分析,非常合情合理!!來看下面這個例子加深印象:
class Echo extends Component {
componentDidMount() {
const parentDom = ReactDOM.findDOMNode(this);
const childrenDom = parentDom.querySelector(".button");
childrenDom.addEventListener('click', this.onDomChildClick, false);
childrenDom.addEventListener('click', this.onDomChildClickCapture, true);
parentDom.addEventListener('click', this.onDomParentClick, false);
parentDom.addEventListener('click', this.onDomParentClickCapture, true);
}
onDomChildClick = (e) => {
console.log('原生事件child--冒泡');
}
onDomChildClickCapture = (e) => {
console.log('原生事件child--捕獲');
}
onDomParentClick = (e) => {
console.log('原生事件parent--冒泡');
}
onDomParentClickCapture = (e) => {
console.log('原生事件parent--捕獲');
}
onReactChildClick = () => {
console.log('合成事件child--捕獲');
}
onReactParentClick = () => {
console.log('合成事件parent--捕獲');
}
render() {
return (
<div className="parent" onClickCapture={this.onReactParentClick}>
<button className="button" onClick={this.onReactChildClick}>點擊</button>
</div>
)
}
}

總結合成事件與原生事件執行順序:
- 合成事件不管冒泡階段還是捕獲階段,都要晚於原生事件冒泡階段
- 不管合成事件還是原生事件,冒泡階段都要晚於捕獲階段
伍 ❀ 阻止原生事件冒泡,會阻斷合成事件執行嗎
相信到這里,你應該能不假思索的回答,如果在原生事件中阻止冒泡,那么事件執行都到不了document
,合成事件自然沒機會去執行了,還是上面那個例子,我們修改如下代碼:
onDomChildClick = (e) => {
e.stopPropagation()
console.log('原生事件child--冒泡');
}
在子元素原生冒泡階段阻止冒泡,可以看到執行如下,整個合成事件都被阻止執行了。

原因其實在上面源碼分析的executeDispatchesInOrder
方法中已經給出了答案:
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
// 如果阻止冒泡,直接break跳出循環
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}}
反過來呢?如果我們在合成事件冒泡階段阻止冒泡,會影響原生事件嗎?我想你心里已經有答案了:
class Echo extends Component {
componentDidMount() {
const parentDom = ReactDOM.findDOMNode(this);
const childrenDom = parentDom.querySelector(".button");
childrenDom.addEventListener('click', this.onDomChildClick, false);
parentDom.addEventListener('click', this.onDomParentClick, false);
}
onDomChildClick = (e) => {
console.log('原生事件child--冒泡');
}
onDomParentClick = (e) => {
console.log('原生事件parent--冒泡');
}
onReactChildClick = (e) => {
e.stopPropagation()
console.log('合成事件child--冒泡');
}
onReactParentClick = (e) => {
console.log('合成事件parent--冒泡');
}
render() {
return (
<div className="parent" onClick={this.onReactParentClick}>
<button className="button" onClick={this.onReactChildClick}>點擊</button>
</div>
)
}
}

那么到這里,我們就解釋了合成事件阻止冒泡對於原生事件的影響,當然在實際開發中,我們盡量還是別混用原生事件與合成事件。
陸 ❀ 總
那么到這里,我大概闡述完了本文想要表達的觀點,本文從最初立初步知識概念,到讀源碼梳理知識點,前前后后花了一個星期的零散時間,說來也是慚愧。不過到最后我對於這塊的知識點還是清楚了一些,至少在面試階段如果有面試官問到,多少是能聊一點了。而文中談到的原生事件是否會阻塞合成事件這個問題,也確實是我同事在面試金山過程中所被問到的問題,現在你也已經知道答案了。
通過本文,你順帶復習了原生事件監聽與代理,也粗略了解了react 16.13.1
版本對於合成事件的底層實現;也正因如此我們順利解釋了原生事件與合成事件的執行差異,以及阻止冒泡對於彼此的影響,那么到這里本文結束。