本文通過一個簡短的實例&控制臺調(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 綁定的結果
說明: 這里的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)用可以查看截圖右側的調(diào)用棧。
_renderSubtreeIntoContainer
-> mountComponentIntoNode
-> mountComponent
[reactReconciler.js] -> _updateDOMProperties
_updateDOMProperties
函數(shù)在mountComponent
拳昌,unmountComponent
和updateComponent
階段都有調(diào)用袭异,它是檢查屬性變化,調(diào)優(yōu)性能的重要方法炬藤。下圖節(jié)選處理事件綁定部分代碼御铃,方法中有指向上次屬性值得lastProp
, nextProp
是當前屬性值,這里nextProp
是我們綁定給組件的onclick
事件處理函數(shù)沈矿。nextProp
不為空調(diào)用enqueuePutListener
綁定事件為空則注銷事件綁定畅买。
enqueuePutListener
這個方法只在瀏覽器環(huán)境下執(zhí)行,傳給listenTo參數(shù)分別是事件名稱'onclick'和代理事件的綁定dom细睡。如果是fragement
就是根節(jié)點(在reactDom.render指定的),不是的話就是document
帝火。listenTo
用于綁定事件到 document 溜徙,下面交由事務處理的是回調(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 對應的瀏覽器原生事件
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
},
對于同一個事件,例如click
有兩個事件 onClick
(在冒泡階段觸發(fā)) onClickCapture
(在捕獲階段觸發(fā))兩個事件名九巡,這個冒泡和捕獲都是react
事件模擬出來的图贸。綁定到 document
上面的事件基本上都是在冒泡階段(對 whell, focus, scroll 有額外處理),如下圖 click
事件綁定執(zhí)行的如下。
topEventMapping
是 topLevlelEvent
瀏覽器事件對照關系冕广,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
上睬辐。
回到上文利用事務存儲事件部分挠阁,這里調(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)使用的),這意味著合成事件對象會被重復使用洪橘,所有的屬性在被調(diào)用以后會被值為null跪者,該機制用于性能優(yōu)化,因此你不可以異步訪問事件熄求。除非調(diào)用event.persist()
渣玲,該方法不會不會把事件放入事件池中,保持event對象不被重置允許代碼的引用到弟晚。
事件觸發(fā)后執(zhí)行dispatchEvent
方法忘衍,該方法第一個參數(shù)是綁定時bind的 topLevelEvent
這里是 topClick
,此處調(diào)用TopLevelCallbackBookKeeping.getPooled
函數(shù)先去事件池中取可以復用的卿城,沒有的話初始化新的枚钓。
這個bookKeeping初始化很簡單,就是把頂級事件類型瑟押,原生事件對象搀捷,空的父組件列表放在一個對象上。
reactUpdate.batchedUpdates
是用事務封裝了handleTopLevelImpl(bookKeeping)
多望。
getEventTarget
返回的是對應的Dom節(jié)點
ReactDOMComponentTree.getClosestInstanceFromNode
返回對應的 reactDomComponent
執(zhí)行事件回調(diào)前,先由當前組件向上遍歷它的所有父組件嫩舟。保存到bookKeeping.ancestors
這個數(shù)組中。因為事件回調(diào)中可能會改變DOM結構,所以要先遍歷好組件層級怀偷,防止與已緩存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ā)相應的事件饭于。第一行是構造出合成事件,第二行就是交由事務處理事件维蒙。
2.1 構建react事件
extractEvent
讓已注冊的plugin
處理相應的的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事件的構造函數(shù)為SyntheticMouseEvent
上文看到 topClick
使用 syntheticMouseEvent
作為事件構造函數(shù)八千。
這里調(diào)用的EventConstructor.getPooled
就是開篇提到的事件池吗讶,先看有沒有可以復用的事件對象沒有的話在重新實例一個燎猛。
這里SyntheticMouseEvent
調(diào)用 SyntheticUIEvent
, SyntheticUIEvent
調(diào)用 SyntheticEvent
。SyntheticEvent
構造函數(shù)這部分代碼相對較長照皆,函數(shù)注釋中說道重绷,該方法應該盡量減少調(diào)用的頻率,使用pooling
(回收再利用|池)機制膜毁。在構建時候會通過判斷isPersistent
屬性來判斷調(diào)用后是否放入池中昭卓。使用者可以通過調(diào)用 persist
方法來改變這個值。
而后執(zhí)行的是 EventPropagators.accumulateTwoPhaseDispatches(event)
這個方法經(jīng)歷層層跳轉瘟滨,詳情可見調(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
运翼。
通過注冊類型獲取到對應類型的所有處理函數(shù)返干,使用.${reactDomComponent._rootNodeID}
找到對應虛擬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ù)后血淌,將它和響應的reactDOMComponent
分別添加到隊列中矩欠。accumulateInto
用于將內(nèi)容添加到現(xiàn)有隊列中,傳入原來隊列和要添加到隊列中的內(nèi)容悠夯。
至此事件已經(jīng)封裝準備好了晚顷。
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í)行后設置 event._dispatchListener
和 event._dispatchInstances
為 null瞬场。
通過EventPluginUtils.getNodeFromInstance
獲取響應的對應的真實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,當可用的對象大于10也不會再往里面添加了契邀。
最后看一下事件的 destructor
方法
這里獲取所有的屬性設置為null摆寄,并且再訪問該事件對象時會預警提醒。
至此事件處理完成坯门。
3 事件機制總結
這里是源碼注釋的翻譯
- 頂級代理是用于捕獲多數(shù)原生瀏覽器事件微饥,這些只會在主線程發(fā)生,并由
reactEventLister
負責處理古戴,reactEventLister 是被注入的因此可以支持插件事件資源欠橘,這是唯一在主線程執(zhí)行的。 - 封裝了頂層事件(TopLevelEvent)來應對瀏覽器異常现恼。這個在工作線程完成肃续。
- 傳遞原生事件以及封裝的頂層事件名稱到
EventPluginHub
,他會遍歷插件是否要執(zhí)行某些合成事件叉袍。 -
EventPluginHub
獲取響應的事件監(jiān)聽器始锚,以及Dom綁定到生成的事件對象上。 -
EventPluginHub
將會派發(fā)事件
3.1 各種事件名
主要三個事件:regiestrationName(注冊事件名)喳逛,topLevelType(頂層事件)瞧捌,(原生事件)
事件綁定階段,從組件屬性中獲取’注冊事件名‘,會區(qū)分捕獲和默認冒泡事件名姐呐,這里的注冊名為react對外暴露的事件殿怜,包含自定義事件。
頂層事件是react
封裝EventPlugin
處理的單位皮钠,react對外暴露的事件是由一個多個事件模擬而成的稳捆。
原生事件是最終綁定到目標元素上的事件,和頂層事件對應關系為一對一的關系麦轰。在綁定給document
的是使用bind函數(shù)乔夯,固定第一個參數(shù)——topLevelEvent的函數(shù)。因此當事件觸發(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
對應的瀏覽器事件末荐,綁定到target
上(如果是fragement
就是根節(jié)點(在reactDom.render指定的),不是的話就是document
)ReactEventListener.dispatchEvent.bind(null, topLevelType)新锈。
3.3 事件存儲
當組件渲染和更新的時候會調(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 事件相關文件
synthetcEvent 封裝合成事件基類
原型方法:
- preventDefault()
- stopPropergation()
- persist() 調(diào)用后
isPersist = true
, 此事件對象將不會被銷毀復用(進入事件池) - 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;
// 為子類添加事件池相關屬性和方法
addEventPoolingTo(Class);
};
- eventPool[]
- getPooled()
參數(shù)同構造函數(shù)傳參缺狠,判斷事件池中是否有可用事件,有的復用脸秽,沒有新建。 - release(event)
判斷事件對象是否isPersist
過 沒有的話調(diào)用對象的destructor
蝴乔, 之后將其添加入事件池记餐。
參考
React源碼分析7 — React合成事件系統(tǒng)
看源碼react事件機制
React源碼解讀系列 – 事件機制
react 合成事件和原生事件的阻止冒泡