本文通過一個簡短的實例&控制臺調(diào)試,了解react事件處理的全過程。下面是測試用代碼,使用控制臺可以清晰看到函數(shù)執(zhí)行過程中參數(shù)變化以及方法所屬模塊&調(diào)用棧,所以本文圖片較多。
class RemoveBtn extends Component {
clickHandler = () => {
this.props.handleClick();
}
render(){
return(
<button onClick={this.clickHandler}>togglage測試組件</button>
)
}
}
class Root extends Component {
clickHandler = () => {
alert('hanlder is1 perform')
}
render(){
return (
<div className="first">
<RemoveBtn handleClick = {this.clickHandler}/>
</div>
)
}
}
ReactDOM.render(<Root />, document.getElementById('root'));
1 事件綁定
1.1 綁定的結(jié)果

說明: 這里的backend.js是react調(diào)試工具的腳本不用考慮。
圖中可見只有在document上綁定了名為dispatchEvent的來自于 ReactEventListener.js模塊的事件處理函數(shù)。
1.2 事件綁定的過程
ReactDOM.render(<Root />, document.getElementById('root'));
一切開始于ReactDOM.render調(diào)用的ReactMount.js的render方法。忽略掉實例化組建的過程,詳細調(diào)用可以查看截圖右側(cè)的調(diào)用棧。

_renderSubtreeIntoContainer -> mountComponentIntoNode -> mountComponent[reactReconciler.js] -> _updateDOMProperties

_updateDOMProperties函數(shù)在mountComponent,unmountComponent和updateComponent階段都有調(diào)用,它是檢查屬性變化,調(diào)優(yōu)性能的重要方法。下圖節(jié)選處理事件綁定部分代碼,方法中有指向上次屬性值得lastProp, nextProp是當(dāng)前屬性值,這里nextProp是我們綁定給組件的onclick事件處理函數(shù)。nextProp 不為空調(diào)用enqueuePutListener綁定事件為空則注銷事件綁定。

enqueuePutListener 這個方法只在瀏覽器環(huán)境下執(zhí)行,傳給listenTo參數(shù)分別是事件名稱'onclick'和代理事件的綁定dom。如果是fragement 就是根節(jié)點(在reactDom.render指定的),不是的話就是document。listenTo 用于綁定事件到 document ,下面交由事務(wù)處理的是回調(diào)函數(shù)的存儲,便于調(diào)用。ReactBrowserEventEmitter 文件中的 listenTo 看做事件處理的源頭。

listenTo: function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
// 獲取 registrationName(注冊事件名稱)的topLevelEvent(頂級事件類型)
var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (dependency === 'topWheel') {
...
} else if (dependency === 'topScroll') {
...
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
...
} else if (topEventMapping.hasOwnProperty(dependency)) {
// 獲取 topLevelEvent 對應(yīng)的瀏覽器原生事件
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
},
對于同一個事件,例如click有兩個事件 onClick(在冒泡階段觸發(fā)) onClickCapture(在捕獲階段觸發(fā))兩個事件名,這個冒泡和捕獲都是react事件模擬出來的。綁定到 document上面的事件基本上都是在冒泡階段(對 whell, focus, scroll 有額外處理),如下圖 click 事件綁定執(zhí)行的如下。

topEventMapping 是 topLevlelEvent 瀏覽器事件對照關(guān)系,mountAt 是綁定對象是函數(shù)接收第二個參數(shù),也就是上文的doc(document)。
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 對所傳的target做了非空判斷后調(diào)用 EventListener.listen 傳參數(shù)分別是:事件對象, 瀏覽器原生事件名稱, 指定了頂級事件類型的事件處理函數(shù)(bind函數(shù))ReactEventListener.dispatchEvent.bind(null, topLevelType)。

EventListener.listen 將事件綁定到target上。
回到上文利用事務(wù)存儲事件部分,這里調(diào)用的putListener方法

調(diào)用 EventPluginHub.putListener 第一個參數(shù)是組件事例,第二個是‘onClick’,第三個是我們寫的事件處理函數(shù)

putListener 將事件處理函數(shù)存儲到listenerBank[registrationName][key]上其中registrationName是事件名稱,.${_rootNodeID}``作為key值,處理函數(shù)作為value存儲。下面調(diào)用的方法有對與safraiclick`事件的兼容處理。
至此事件綁定告一段落了。
2 事件處理
event pooling事件池
合成事件是 pooled(循環(huán)使用的),這意味著合成事件對象會被重復(fù)使用,所有的屬性在被調(diào)用以后會被值為null,該機制用于性能優(yōu)化,因此你不可以異步訪問事件。除非調(diào)用event.persist(),該方法不會不會把事件放入事件池中,保持event對象不被重置允許代碼的引用到。
事件觸發(fā)后執(zhí)行dispatchEvent方法,該方法第一個參數(shù)是綁定時bind的 topLevelEvent這里是 topClick,此處調(diào)用TopLevelCallbackBookKeeping.getPooled函數(shù)先去事件池中取可以復(fù)用的,沒有的話初始化新的。

這個bookKeeping初始化很簡單,就是把頂級事件類型,原生事件對象,空的父組件列表放在一個對象上。

reactUpdate.batchedUpdates是用事務(wù)封裝了handleTopLevelImpl(bookKeeping)。


getEventTarget 返回的是對應(yīng)的Dom節(jié)點
ReactDOMComponentTree.getClosestInstanceFromNode 返回對應(yīng)的 reactDomComponent

執(zhí)行事件回調(diào)前,先由當(dāng)前組件向上遍歷它的所有父組件。保存到bookKeeping.ancestors這個數(shù)組中。因為事件回調(diào)中可能會改變DOM結(jié)構(gòu),所以要先遍歷好組件層級,防止與已緩存ReactMount's node相矛盾。之后就是依次掉調(diào)用 ReactEventListener._handleTopLevel

最后一個參數(shù)通過getEventTarget函數(shù)兼容svg以及safrai 的 textNode 這里最終返回的是觸發(fā)事件的DOM節(jié)點。

handleTopLevel函數(shù)經(jīng)由EventPluginHub處理 top level Event,在EventPluginHub處理過程中不同的plugin可以創(chuàng)建派發(fā)相應(yīng)的事件。第一行是構(gòu)造出合成事件,第二行就是交由事務(wù)處理事件。

2.1 構(gòu)建react事件
extractEvent 讓已注冊的plugin處理相應(yīng)的的topLevelType。下圖看到在運行過程中已注冊的plugin只有五個分別是
ReactInjection.EventPluginHub.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin
});

extractEvent會依次調(diào)用每個plugin的extractEvents方法,第一個處理的是SimpleEventPlugin,該plugin處理了絕大部分的事件,本例 onClick 就是其中之一。

經(jīng)由一個switch(topLevelType)確定該react事件的構(gòu)造函數(shù)為SyntheticMouseEvent

上文看到 topClick 使用 syntheticMouseEvent 作為事件構(gòu)造函數(shù)。

這里調(diào)用的EventConstructor.getPooled就是開篇提到的事件池,先看有沒有可以復(fù)用的事件對象沒有的話在重新實例一個。

這里SyntheticMouseEvent調(diào)用 SyntheticUIEvent, SyntheticUIEvent調(diào)用 SyntheticEvent。SyntheticEvent構(gòu)造函數(shù)這部分代碼相對較長,函數(shù)注釋中說道,該方法應(yīng)該盡量減少調(diào)用的頻率,使用pooling(回收再利用|池)機制。在構(gòu)建時候會通過判斷isPersistent屬性來判斷調(diào)用后是否放入池中。使用者可以通過調(diào)用 persist方法來改變這個值。
而后執(zhí)行的是 EventPropagators.accumulateTwoPhaseDispatches(event)
這個方法經(jīng)歷層層跳轉(zhuǎn),詳情可見調(diào)用棧,最后到traverseTwoPhase這個函數(shù)。inst 為 觸發(fā)事件的reactDomComponent,fn 為 accumulateDirectionDispatches, arg 為合成事件。
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = inst._hostParent;
}
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);
}
}
path 為收集的以target起始到根節(jié)點為止的組件,本例中兩個。用于后續(xù)模擬事件的捕獲和冒泡。

之后按照從外到內(nèi)捕獲從里到外冒泡的順序調(diào)用 accumulateDirectionDispatches(path[i], 'captured', arg)該方法將合成事件與處理函數(shù)聯(lián)系起來。

這里 listenerAtPhase -> getListener[EventPluginHub.js] 獲取事件處理函數(shù)。
在事件綁定中最后把所有的事件處理放在一個對象上listenerBank。

通過注冊類型獲取到對應(yīng)類型的所有處理函數(shù),使用.${reactDomComponent._rootNodeID} 找到對應(yīng)虛擬Dom上的事件處理函數(shù)。
/**
* @param {object} inst reactDOMComponent 實例 (虛擬DOM)
* @param {string} registrationName 注冊事件名
* * /
getListener: function (inst, registrationName) {
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
// 獲取同類型的所有處理函數(shù)
var bankForRegistrationName = listenerBank[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
return null;
}
// 獲取 .${reactDomComponent._rootNodeID}`
var key = getDictionaryKey(inst);
// 返回指定虛擬DOM上的事件處理函數(shù)
return bankForRegistrationName && bankForRegistrationName[key];
}
獲取事件處理函數(shù)后,將它和響應(yīng)的reactDOMComponent分別添加到隊列中。accumulateInto用于將內(nèi)容添加到現(xiàn)有隊列中,傳入原來隊列和要添加到隊列中的內(nèi)容。

至此事件已經(jīng)封裝準(zhǔn)備好了。
2.2 事件分發(fā)

承接上文封裝好的event對象。使用runEventQueueInBatch開始事件分發(fā)。

這里第一行用于將事件放入隊列processEventQueue中,其內(nèi)部調(diào)用的還是accumulateInto方法。
第二行,processEventQueue派發(fā)所有在事件隊列processEventQueue中的合成事件。

首先將隊列中的內(nèi)容取出,清空隊列,以防處理中隊列變化。
simulated:為true表示React測試代碼,我們一般都是false
此注解出自參考文章一
這里forEachAccumulate就是對第一個參數(shù)執(zhí)行foreach調(diào)用第二個參數(shù)。

executeDispatchesAndReleaseTopLevel -> executeDispatchesAndRelease 該函數(shù) -> EventPluginUtils.executeDispatchesInOrder,并將沒有調(diào)用persist的事件對象回收到事件池。

處理函數(shù)是多個,則依次執(zhí)行。本例中只有一個處理函數(shù) -> executeDispatch。執(zhí)行后設(shè)置 event._dispatchListener 和 event._dispatchInstances 為 null。

通過EventPluginUtils.getNodeFromInstance獲取響應(yīng)的對應(yīng)的真實DOM節(jié)點作為事件的currentTarget。
本例執(zhí)行85行 這里的type 為click,func為事件處理函數(shù), event為合成事件對象。
在生產(chǎn)環(huán)境中,會直接調(diào)用事件處理函數(shù),開發(fā)環(huán)境中會模擬瀏覽器事件。

模擬過程如下。

這里在創(chuàng)建的fakeElement上綁定事件,之后模擬事件觸發(fā)(執(zhí)行本例中的事件處理函數(shù)),再注銷事件綁定。
到此為止這個事件已經(jīng)處理完,接下來就是把這個事件屬性置為null,然后把它放入事件池中了。判斷是否強制了調(diào)用了persistent,沒有的話就釋放事件對象。


其實這里可以看到事件池有一個上線就是10,當(dāng)可用的對象大于10也不會再往里面添加了。
最后看一下事件的 destructor 方法

這里獲取所有的屬性設(shè)置為null,并且再訪問該事件對象時會預(yù)警提醒。

至此事件處理完成。
3 事件機制總結(jié)
這里是源碼注釋的翻譯
- 頂級代理是用于捕獲多數(shù)原生瀏覽器事件,這些只會在主線程發(fā)生,并由
reactEventLister負責(zé)處理,reactEventLister 是被注入的因此可以支持插件事件資源,這是唯一在主線程執(zhí)行的。 - 封裝了頂層事件(TopLevelEvent)來應(yīng)對瀏覽器異常。這個在工作線程完成。
- 傳遞原生事件以及封裝的頂層事件名稱到
EventPluginHub,他會遍歷插件是否要執(zhí)行某些合成事件。 -
EventPluginHub獲取響應(yīng)的事件監(jiān)聽器,以及Dom綁定到生成的事件對象上。 -
EventPluginHub將會派發(fā)事件
3.1 各種事件名
主要三個事件:regiestrationName(注冊事件名),topLevelType(頂層事件),(原生事件)
事件綁定階段,從組件屬性中獲取’注冊事件名‘,會區(qū)分捕獲和默認冒泡事件名,這里的注冊名為react對外暴露的事件,包含自定義事件。
頂層事件是react封裝EventPlugin處理的單位,react對外暴露的事件是由一個多個事件模擬而成的。
原生事件是最終綁定到目標(biāo)元素上的事件,和頂層事件對應(yīng)關(guān)系為一對一的關(guān)系。在綁定給document的是使用bind函數(shù),固定第一個參數(shù)——topLevelEvent的函數(shù)。因此當(dāng)事件觸發(fā)后使用的。
// 本例中
// regiestrationName(注冊名)
onClick
onClickCapture
// topLevelType (頂層事件類型)
topClick
// native Event (原生事件) | dependence
click
// regiestrationName => topLevelType
EventPluginRegisterName.registionNameDependencies
// topLevelType => native event
topEventMapping[位于reactBrowserEventEmitter.js]
3.2 事件全局代理(target)
根據(jù)不同的topLevelType對應(yīng)的瀏覽器事件,綁定到target上(如果是fragement 就是根節(jié)點(在reactDom.render指定的),不是的話就是document)ReactEventListener.dispatchEvent.bind(null, topLevelType)。
3.3 事件存儲
當(dāng)組件渲染和更新的時候會調(diào)用_updateDomPorperties方法檢查屬性變化,這里執(zhí)行reactBrowerEventEmitter模塊下的listenTo對不同事件進行了兼容處理后最終調(diào)用 EventPluginHub.js模塊下的 putListener方法,將事件處理函數(shù),以 .${reactDomComponent._rootNodeID}為key值放在listenerBank[registrationName]對象上。
3.4 阻止事件冒泡
通過事件綁定的分析會發(fā)現(xiàn),無論注冊的是onClick 還是 onClickCapture 最后都是調(diào)用 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 在冒泡階段觸發(fā)的事件, 也會發(fā)現(xiàn)在沒有執(zhí)行事件處理函數(shù)的時候,事件就已經(jīng)eventQuene中,那是不是就意味著調(diào)用e.stopPropagation()就不能阻止事件冒泡了呢。
事件處理函數(shù)中獲取的事件是合成事件對象,合成事件對象也是有stopPropagation方法的。

注意這里的最后一行,這里執(zhí)行的函數(shù)為事件isPropagationStopped方法賦值了一個只會返回true的函數(shù)。而在一次調(diào)用事件處理函數(shù)的過程中,每一次都會調(diào)用事件對象的該方法。

因此使用e.stopPropagation()不能組織原生事件冒泡,但是模擬到阻止事件冒泡的效果的。
react 文檔說明
更多可參考[4]
3.5 事件相關(guān)文件
synthetcEvent 封裝合成事件基類
原型方法:
- preventDefault()
- stopPropergation()
- persist() 調(diào)用后
isPersist = true, 此事件對象將不會被銷毀復(fù)用(進入事件池) - isPersist
- desturctor() 事件觸發(fā)后(isPersist!==true), 清空事件對象屬性。
**靜態(tài)方法: **
- arugumentClass
// @prarm interface 需要定義的事件對象屬性
// @param Class 子類
SyntheticEvent.augmentClass = function(Class, Interface) {
var Super = this;
var E = function() {};
E.prototype = Super.prototype;
var prototype = new E();
Object.assign(prototype, Class.prototype);
// 子類繼承基類原型上的方法
Class.prototype = prototype;
Class.prototype.constructor = Class;
// 合并interface
Class.Interface = Object.assign({}, Super.Interface, Interface);
Class.augmentClass = Super.augmentClass;
// 為子類添加事件池相關(guān)屬性和方法
addEventPoolingTo(Class);
};
- eventPool[]
- getPooled()
參數(shù)同構(gòu)造函數(shù)傳參,判斷事件池中是否有可用事件,有的復(fù)用,沒有新建。 - release(event)
判斷事件對象是否isPersist過 沒有的話調(diào)用對象的destructor, 之后將其添加入事件池。
參考
React源碼分析7 — React合成事件系統(tǒng)
看源碼react事件機制
React源碼解讀系列 – 事件機制
react 合成事件和原生事件的阻止冒泡