本人研究的源代碼是0.8.0版本的,可能跟最新版本的事件系統有點出入。javascript
首先,合成事件這個名詞是從「Synthetic Event」翻譯過來的,在react的官方文檔和源碼中,這個術語狹義上是指合成事件對象,一個普通的javascript對象。而在這裏,咱們談論的是由衆多不一樣類型事件的合成事件對象組成的合成事件系統(React’s Event System)。在個人理解裏面,合成事件是相對瀏覽器原生的事件系統而言的。合成事件系統本質上是遵循W3C的相關規範,把瀏覽器實現過的事件系統再實現一遍,並抹平各個瀏覽器的實現差別,使得開發者使用起來體驗是一致的。html
在開始理解什麼是合成事件系統以前,咱們不妨看看我翻譯的react的合成事件對象。從這篇文檔,咱們能夠獲得如下關於合成事件系統與原生事件系統異同方面的結論:html5
event target
,current event target
, event object
,event phase
和 propagation path
等核心概念上的定義是一致的。propagation path
上傳播。換句話說,就是同一個propagation path
上的每個event listener拿到的event object
都是同一個。也就是說,這二者採用的架構,實現的接口都是一致的。由於二者都遵循W3C的標準規範。java
在這裏之因此要提到react合成事件系統與原生系統上的異同點,這是由於我以爲帶着「形成二者之間的差異的緣由是什麼呢?」這個疑問去探索react的合成事件系統會更有針對性。由於源碼每每是繁複的,如同茫然而無邊際的原始森林通常,一旦咱們沒有目標,就容易迷失在這原始森林裏,最終一無所得。node
在ReactEventEmitter.js的源碼中,官方給出了這樣的架構圖:react
+------------+ .
| DOM | .
+---^--------+ . +-----------+
| + . +--------+|SimpleEvent|
| | . | |Plugin |
+---|--|------+ . v +-----------+
| | | | . +--------------+ +------------+
| | +-------------->|EventPluginHub| | Event |
| | . | | +-----------+ | Propagators|
| ReactEvent | . | | |TapEvent | |------------|
| Emitter | . | |<---+|Plugin | |other plugin|
| | . | | +-----------+ | utilities |
| +-----------.---------+ | +------------+
| | | . +----|---------+
+-----|------+ . | ^ +-----------+
| . | | |Enter/Leave|
+ . | +-------+|Plugin |
+-------------+ . v +-----------+
| application | . +----------+
|-------------| . | callback |
| | . | registry |
| | . +----------+
+-------------+ .
.
React Core . General Purpose Event Plugin System
複製代碼
從官方給出的架構圖,咱們能夠看到如下主要的角色:程序員
下面咱們來分析一下他們之間的關係。web
注意,原架構圖,是沒有從ReactEventEmitter到DOM的關係鏈的,是本人添加的。編程
從ReactEventEmitter指向DOM的關係是指,ReactEventEmitter負責向DOM的頂層進行事件委託。該關係鏈對應的大致調用棧是(序號越小,表示越先調用):數組
「DOM -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry」這條關係鏈指的是當用戶跟DOM交互觸發了原生事件的時候,由於ReactEventEmitter經過createTopLevelCallback方法事先在top level上註冊了各類事件的監聽器,因此,最早通知到的是ReactEventEmitter。而後纔是ReactEventEmitter通知EventPluginHub去找到相應的event plugin,讓它去合成此次事件dispatch所須要event object,而後執行dispatch任務。在dispatch任務的執行過程當中,EventPluginHub須要從CallbackRegistry中找到對應的event listener(或者稱之爲event callback)並調用它。 該關係鏈對應的大致調用棧是:
「application -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry」這條關係存在於application event listener存儲階段。ReactEventEmitter負責在react component的首次掛載階段對開發者寫的event listener進行收集和存儲。在我看來,只須要存在「application -> CallbackRegistry」的關係就好,不知道源碼中爲何利用引用傳遞,繞來繞去,把整個關係鏈延伸得這麼長。該關係鏈對應的大致調用棧是:
「xxxEventPlugin」與EventPluginHub的關係。
各類「xxxEventPlugin」是被注入(inject)到EventPluginHub裏面的,換句話說,就是「xxxEventPlugin」的引用會被掛載在EventPluginHub.registrationNames對象的各個key上。注入後的具體數據結構是這樣的:
EventPluginHub.registrationNames = {
onBlur: xxxEventPlugin,
onBlurCapture: xxxEventPlugin,
onChange: xxxEventPlugin,
onChangeCapture: xxxEventPlugin,
......
}
複製代碼
在v0.8.0的源碼中,eventPlugin主要有如下幾個:
既然「xxxEventPlugin」是被注入到EventPluginHub裏面的,那麼咱們不由問,是在哪裏被注入的呢?答曰:是在react.js初始化階段,react根組建沒有被初始掛載以前完成的。具體代碼在ReactDefaultInjection.js裏面:
/**
* Some important event plugins included by default (without having to require
* them).
*/
EventPluginHub.injection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
CompositionEventPlugin: CompositionEventPlugin,
MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
SelectEventPlugin: SelectEventPlugin
});
複製代碼
從集合的概念上講,EventPluginHub與eventPlugin的關係是「一對多」的關係。全部的eventPlugin都要注入到EventPluginHub中去。
「xxxEventPlugin」與「SyntheticxxxEvent」的關係
EventPlugin在合成event object的時候,不一樣類型的事件,須要調用不一樣的合成事件的構造函數,也就是說,「xxxEventPlugin」與「SyntheticxxxEvent」造成了一對多的關係。咱們拿SimpleEventPlugin的extractEvents方法作個示例:
/** * @param {string} topLevelType Record from `EventConstants`. * @param {DOMEventTarget} topLevelTarget The listening component root node. * @param {string} topLevelTargetID ID of `topLevelTarget`. * @param {object} nativeEvent Native browser event. * @return {*} An accumulation of synthetic events. * @see {EventPluginHub.extractEvents} */ extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { return null; } var EventConstructor; switch(topLevelType) { case topLevelTypes.topInput: case topLevelTypes.topSubmit: // HTML Events // @see http://www.w3.org/TR/html5/index.html#events-0 EventConstructor = SyntheticEvent; break; case topLevelTypes.topKeyDown: case topLevelTypes.topKeyPress: case topLevelTypes.topKeyUp: EventConstructor = SyntheticKeyboardEvent; break; case topLevelTypes.topBlur: case topLevelTypes.topFocus: EventConstructor = SyntheticFocusEvent; break; case topLevelTypes.topClick: // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return null; } /* falls through */ case topLevelTypes.topContextMenu: case topLevelTypes.topDoubleClick: case topLevelTypes.topDrag: case topLevelTypes.topDragEnd: case topLevelTypes.topDragEnter: case topLevelTypes.topDragExit: case topLevelTypes.topDragLeave: case topLevelTypes.topDragOver: case topLevelTypes.topDragStart: case topLevelTypes.topDrop: case topLevelTypes.topMouseDown: case topLevelTypes.topMouseMove: case topLevelTypes.topMouseUp: EventConstructor = SyntheticMouseEvent; break; case topLevelTypes.topTouchCancel: case topLevelTypes.topTouchEnd: case topLevelTypes.topTouchMove: case topLevelTypes.topTouchStart: EventConstructor = SyntheticTouchEvent; break; case topLevelTypes.topScroll: EventConstructor = SyntheticUIEvent; break; case topLevelTypes.topWheel: EventConstructor = SyntheticWheelEvent; break; case topLevelTypes.topCopy: case topLevelTypes.topCut: case topLevelTypes.topPaste: EventConstructor = SyntheticClipboardEvent; break; } ("production" !== process.env.NODE_ENV ? invariant( EventConstructor, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType ) : invariant(EventConstructor)); var event = EventConstructor.getPooled( dispatchConfig, topLevelTargetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 複製代碼
從上面的源碼能夠看到,extractEvents方法會根據不一樣的事件類型,使用不一樣的「SyntheticxxxEvent」構造函數來構造合成事件對象。SimpleEventPlugin用到的構造函數有如下:
EventPluginHub,eventPlugin和SyntheticEvent三者之間的關係以下:
在對每一個階段進行分析前,我先作個預告。預告一下幾種數據結構和兩種關係。
listenerBank = { onClick: { '[0].[1]': listener // listener就是咱們掛載在jsx的事件回調 }, onClickCapture: { '[0].[1]': listener } } 複製代碼
正如上面我提到的,閱讀源碼必須有一個聚焦的目標。咱們不妨從註冊在react component上的event listener的身上出發,探究一下,從咱們註冊開始,到event listener被調用的這個過程,咱們的event listener到底經歷了什麼?通過研究整理,咱們能夠把event listener這個生命週期劃分爲四個階段:
準備階段
1.1. 各類eventPlugin的提早注入(依賴注入)
1.2. 提早在document上對全部已支持的事件進行監聽(事件委託)
存儲階段
2.1. 找到收集入口
2.2. 儲存application event listener
調用階段
3.1. 根據eventType去查找eventPlugin
3.2. 組建eventQueue(由組建不一樣類型事件的events組成)
第一步:構造SyntheticEvent的實例;
第二步:往SyntheticEvent的實例添加各類加強屬性和用於抹平跨瀏覽器差別的兼容屬性;
第三步: 分別沿着event target的捕獲階段傳播路徑和冒泡階段傳播路勁去取回application event listener,並按照先捕獲,後冒泡的順序推入到存放listener的隊列中,也就是event._dispatchListeners。
3.3. 循環eventQueue,依次dispatch每個SyntheticEvent
收尾階段
主要是對eventQueue作垃圾回收,釋放SyntheticEvent實例,讓它從新回到pooling池中。
準備階段作了兩件事:
由於react的開發者們很早就考慮到react要應用到跨平臺開發中,因此,他們很早就着手分離react的核心代碼和平臺相關的的代碼了。早期採用的依賴注入模式和後期採用的分包模式,就是他們進行跨平臺架構所採用的主要手段。在react合成事件系統中,對EventPluginHub的實現就是採用了這種依賴注入模式組織而成的。在react.js程序入口出,咱們對EventPluginHub的依賴進行了注入(見ReactDefaultInjection.js開頭):
function inject() { ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback; /** * Inject module for resolving DOM hierarchy and plugin ordering. */ EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder); EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); /** * Some important event plugins included by default (without having to require * them). */ EventPluginHub.injection.injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, CompositionEventPlugin: CompositionEventPlugin, MobileSafariClickEventPlugin: MobileSafariClickEventPlugin, SelectEventPlugin: SelectEventPlugin }); // ......other code here } 複製代碼
從代碼中,咱們能夠看出,咱們往EventPluginHub裏面注入了EventPluginOrder,InstanceHandle和本平臺(web)所須要用到的全部eventPlugin。eventPlugin是用來幹嗎的,這裏就不重複解釋了,上面說過。咱們在裏說說剩餘的兩個:EventPluginOrder和InstanceHandle。
咱們直譯就是「事件插件順序」的意思。其實,更確切地說,應該是指「事件插件的加載順序」。這個被注入的順序是怎麼的呢?見源碼DefaultEventPluginOrder.js:
/** * Module that is injectable into `EventPluginHub`, that specifies a * deterministic ordering of `EventPlugin`s. A convenient way to reason about * plugins, without having to package every one of them. This is better than * having plugins be ordered in the same order that they are injected because * that ordering would be influenced by the packaging order. * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that * preventing default on events is convenient in `SimpleEventPlugin` handlers. */ var DefaultEventPluginOrder = [ keyOf({ResponderEventPlugin: null}), keyOf({SimpleEventPlugin: null}), keyOf({TapEventPlugin: null}), keyOf({EnterLeaveEventPlugin: null}), keyOf({ChangeEventPlugin: null}), keyOf({SelectEventPlugin: null}), keyOf({CompositionEventPlugin: null}), keyOf({AnalyticsEventPlugin: null}), keyOf({MobileSafariClickEventPlugin: null}) ]; 複製代碼
轉換一下,DefaultEventPluginOrder最後的值是這樣的:
var DefaultEventPluginOrder = [ 'ResponderEventPlugin', 'SimpleEventPlugin', 'TapEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'CompositionEventPlugin', 'AnalyticsEventPlugin', 'MobileSafariClickEventPlugin' ]; 複製代碼
也就是說,咱們須要eventPlugin按照上面的順序加載並執行。爲何須要規定eventPlugin按照必定的順序加載,執行呢?從源碼的註釋上咱們不難找到問題的答案。那就是:某些plugin須要先於某些plugin加載並執行。好比ResponderEventPlugin就必須在SimpleEventPlugin加載以前加載,不然SimpleEventPlugin負責處理的event listenter裏面就沒法阻止事件的默認行爲。由於每一次打包的順序是沒辦法保證100%都是一致的,因此手動地按照順序引入每個plugin,手動地按照順序注入每個plugin的這種方案也是不太可靠的。相比之下,顯示地聲明一個plugin的加載順序,而後手動地調用publishRegistrationName方法來加載plugin,這種方案更好。
從上面給出的源代碼:
EventPluginHub.injection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
CompositionEventPlugin: CompositionEventPlugin,
MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
SelectEventPlugin: SelectEventPlugin
});
複製代碼
咱們能夠看出,咱們總共用到了SimpleEventPlugin,EnterLeaveEventPlugin,ChangeEventPlugin,CompositionEventPlugin,MobileSafariClickEventPlugin和SelectEventPlugin這六個plugin。而它們加載的順序就是咱們上面給出的順序:
var DefaultEventPluginOrder = [ 'ResponderEventPlugin', 'SimpleEventPlugin', 'TapEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'CompositionEventPlugin', 'AnalyticsEventPlugin', 'MobileSafariClickEventPlugin' ]; 複製代碼
InstanceHandle是一個utils模塊。它主要包含着一些用於處理react instance方面需求的工具函數。好比:createReactRootID,getReactRootIDFromNodeID,traverseTwoPhase等等。其中,traverseTwoPhase跟react的合成事件系統關係最爲緊密。它將會被用到咱們上面所提到的第三階段。它主要負責,給定一個reactID,它能從這個reactID所對應的節點出發,沿着捕獲路徑和冒泡路徑去查找並收集註冊到當前事件的event listener,並將它們按照正確的順序入隊到存放event listener的隊列裏面去。這部分的細節,咱們將會在第三階段那裏詳細地闡述。
在javascript中,依賴注入的本質是引用傳遞。所以,咱們能夠說js的依賴注入是隱式的引用傳遞。縱觀EventPluginHub的內部代碼,你會發現,裏面存在大量的引用傳遞。EventPluginHub
這個模塊,就像一個甩手掌櫃同樣,其實啥大事也沒有幹,它都把它的大部分工做交給了CallbackRegistry
和EventPluginRegistry
。這個場景讓我想起了一個對於中國程序員來講甚是美麗而悲傷的「故事」。在這個故事裏面,Bob跟EventPluginHub
同樣,沒作太多事情,卻躺贏了人生。
在第一階段,除了對EventPluginHub進行了依賴注入,還對ReactEventEmitter也進行了依賴注入。
ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback;
複製代碼
注入的ReactEventTopLevelCallback方法用於建立綁定在top level上event listener。咱們會在下面的事件委託部分進行詳細的闡述。
事件委託模式已是咱們的老朋友了,在jQuery時代,咱們早就接觸過了。事件委託原理的核心要素是原生事件的「事件冒泡」機制和「event target」。事件委託的總體流程大致以下:
此時,咱們基本上能夠看清「委託(delegation)」的含義。若是說「調用event listener」是一項須要完成的事情的話,那麼相比於咱們本身來作(直接在原生DOM元素上監聽,等待瀏覽器直接調用咱們的event listener),咱們如今把這件事「委託」給了這個原生DOM元素的祖先元素,讓它在它的事件回調被瀏覽器調用時,間接地來調用咱們的event listenter。
也許你會問:「原生DOM元素的祖先元素爲何會有它的事件回調呢?」。
答:「固然是須要咱們(指的是像jQuery和react這樣的類庫)事先手動地作事件監聽啦」;
也許你又會問:「當用戶點擊原生DOM元素的時候,爲何它的祖先元素的事件回調會執行呢?」。
答:「由於有「事件冒泡」這一機制在」;
也許你還會問:「全部元素都將事件監聽委託給同一個祖先元素,那麼當事件觸發時,該祖先元素是怎麼知道該調用哪些event listener呢?」。
答:根據原生的event對象的target屬性,咱們能夠先肯定事件傳播的路徑,再收集該路徑上全部元素的綁定的event listener便可。
不管在jQuery中,仍是react中,事件委託的運行流程大抵跟上面提到的差很少。 咱們此處要說的其實是指這四個流程裏面的第一步。也就是react合成事件系統中所說的「listen at top level」。
源碼上是用了「top level」這個術語,一番源碼查閱下來,它其實就是指「document對象」。下面看源碼(在ReactMount.js裏面):
prepareEnvironmentForDOM: function(container) { ("production" !== process.env.NODE_ENV ? invariant( container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ), 'prepareEnvironmentForDOM(...): Target container is not a DOM element.' ) : invariant(container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ))); // 注意:document.documentElement的nodeType也是1 // 此處是爲了獲取文檔對象:document var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container; ReactEventEmitter.ensureListening(ReactMount.useTouchEvents, doc); } 複製代碼
正如上面註釋所說,這個doc變量的值最終是document對象。若是你往調用棧追溯下去的話:
你會發現,咱們的doc會被傳入到一個叫listen的方法裏面:
到這裏,咱們也看到了熟悉的原生方法「addEventListener」了,咱們也就能夠肯定,這個所謂的「top level」就是document對象了。
好,既然咱們肯定了「top level」就是document對象了。那麼接下來就是探究一下如何「listen at」了。
我也不賣關子了,其實react的「listen at」就是枚舉式地,一個個地在document對象上,對目前全部的瀏覽器事件作了事件監聽。直接上源代碼(在ReactEventEmitter.js裏面):
listenAtTopLevel: function(touchNotMouse, contentDocument) { ("production" !== process.env.NODE_ENV ? invariant( !contentDocument._isListening, 'listenAtTopLevel(...): Cannot setup top-level listener more than once.' ) : invariant(!contentDocument._isListening)); var topLevelTypes = EventConstants.topLevelTypes; var mountAt = contentDocument; registerScrollValueMonitoring(); trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt); trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt); trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt); trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt); trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt); trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt); trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt); trapBubbledEvent(topLevelTypes.topContextMenu, 'contextmenu', mountAt); if (touchNotMouse) { trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt); trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt); trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt); trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt); } trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt); trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt); trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt); trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt); trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt); trapBubbledEvent( topLevelTypes.topSelectionChange, 'selectionchange', mountAt ); trapBubbledEvent( topLevelTypes.topCompositionEnd, 'compositionend', mountAt ); trapBubbledEvent( topLevelTypes.topCompositionStart, 'compositionstart', mountAt ); trapBubbledEvent( topLevelTypes.topCompositionUpdate, 'compositionupdate', mountAt ); if (isEventSupported('drag')) { trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt); trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt); trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt); trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt); trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt); trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt); trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt); trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt); } if (isEventSupported('wheel')) { trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); } else if (isEventSupported('mousewheel')) { trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); } else { // Firefox needs to capture a different mouse scroll event. // @see http://www.quirksmode.org/dom/events/tests/scroll.html trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); } // IE<9 does not support capturing so just trap the bubbled event there. if (isEventSupported('scroll', true)) { trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); } else { trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); } if (isEventSupported('focus', true)) { trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); } else if (isEventSupported('focusin')) { // IE has `focusin` and `focusout` events which bubble. // @see // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); } if (isEventSupported('copy')) { trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt); trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt); trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt); } } 複製代碼
從上面的代碼中,咱們能夠看到,react幾乎對咱們所熟知的事件分別在冒泡階段和捕獲階段(如何支持的話)做了事件監聽。topLevelType的事件名就是將原生的事件名改成小駝峯的寫法,而且在前面加上「top」前綴。爲了直觀,我在createTopLevelCallback方法中把全部的topLevelType答應出來看看:
一番探索下來,react合成事件系統中的「listen at top level」其實也沒有想象中的那麼高深,現在看起來,甚至有些笨拙。由於react合成事件系統是採用了事件委託模式,而且topLevelType是註冊在事件的冒泡階段,因此咱們能夠得出如下結論:
下面,我以click事件爲例,驗證一下結論1:
// 在應用代碼中打log handleClick=()=> {console.log('btn react click event')}} handleClickCapture=()=> {console.log('btn react clickCapture event')}} render() { return ( <button id="btn" onClick={this.handleClick} onClickCapture={this.handleClickCapture} > 點我試一試 </button> ) } // 在源碼中打log createTopLevelCallback: function createTopLevelCallback(topLevelType) { return function (nativeEvent) { if (nativeEvent.type === 'click') { console.log('document native click callback'); } if (!_topLevelListenersEnabled) { return; } // TODO: Remove when synthetic events are ready, this is for IE<9. if (nativeEvent.srcElement && nativeEvent.srcElement !== nativeEvent.target) { nativeEvent.target = nativeEvent.srcElement; } var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(nativeEvent)) || window; var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; ReactEventEmitter.handleTopLevel(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent); }; } 複製代碼
最後打印出來的結果是:
document native click callback
btn react clickCapture event
btn react click event
複製代碼
從而驗證告終論1是正確的。
下面,咱們再來驗證一下結論2:
在進行驗證以前,咱們要明白這麼一個現象:「用戶一個交互動做,可能會觸發多個事件」。好比,「點擊按鈕」的這麼一個交互動做,對於了按鈕元素來講,就有可能觸發「mousedown」, 「mouseup」, 「click」。在react的合成事件系統裏面,還會多個「focus」事件。
handleMousedown=()=> {console.log('react mousedown event')} handleMouseup=()=> {console.log('react mouseup event')} handleClick=()=> {console.log('react click event')}} render() { return ( <button id="btn" onMouseDown={this.handleMousedown} onMouseUp={this.handleMouseup} onClick={this.handleClick} > 點我試一試 </button> ) } componentDidMount(){ const btn = doucument.getElementById('btn'); btn.addEventListener('mousedown',()=> { console.log('native mousedown event')}); btn.addEventListener('mouseup',()=> { console.log('native mouseup event')}); btn.addEventListener('click',()=> { console.log('native click event')}); } 複製代碼
打印結果以下:
native mousedown event
react mousedown event
native mouseup event
react mouseup event
native click event
react click event
複製代碼
那麼你會發現react的event listener的調用順序跟原生的event listener的調用順序是一致的,從而驗證告終論2是正確的。
由於事件委託模式的運行有賴於瀏覽器原生的事件冒泡機制,那咱們不由問,假如咱們在某個事件的冒泡路徑上阻止了事件傳播,那麼react的event listener是否是就不會執行啦?咱們不妨使用下面代碼來驗證一下:
handleClick=()=> {console.log('react click event')}} render() { return ( <button id="btn" onMouseDown={this.handleMousedown} onMouseUp={this.handleMouseup} onClick={this.handleClick} > 點我試一試 </button> ) } componentDidMount(){ const btn = doucument.getElementById('btn'); btn.addEventListener('click',(event)=> { event.stopPropragation(); }); } 複製代碼
以上代碼執行後,你會發現,點擊button,react的event listener就不執行了。這是由於註冊在document對象上的topLevelCallback並無執行。若是咱們把event.stopPropragation()
語句註釋了,那麼控制檯就會從新打印出react click event
。這從而證實了事件委託模式的坑仍是有點深的:若是你在開發過程當中,原生事件監聽與react事件監聽混用,一不當心寫出這種代碼的話,那麼你這個button事件傳播路徑上的全部的react event listener都不會執行了。
至此,react合成系統運行的第一階段已經講解完畢了,下面咱們進入第二階段的講解。
用戶(也就是開發者)註冊的event listener通常稱之爲「application event listener」,下面,咱們簡稱爲「event listener」。而存儲階段就是指從react element身上收集,並存儲在事件監聽登記表上的過程。
首先,看看在react中,咱們是怎樣地註冊事件監聽的。若是是寫成jsx的話,那麼是這樣的:
handleClick=()=> {console.log('react click event')}} render() { return ( <button id="btn" onClick={this.handleClick} > 點我試一試 </button> ) } 複製代碼
假如咱們換成js的寫法,更能看透react事件監聽的原生面貌:
handleClick=()=> {console.log('react click event')}} render() { return React.DOM.button({ id: 'btn', onClick: this.handleClick }, '點我試一試'); } 複製代碼
jsx寫法很像DOM1的事件監聽,若是咱們不假思索,很容易被感受所迷惑。覺得本身在寫着一些原生的事件監聽的代碼。其實否則。說到底,react的事件監聽寫法本質就是對象裏面的key-value對。key是「onClick」,value是event listener的函數引用。把函數當成值來使用,是javascript編程的一大特點。所以,我嗯能夠在不看源碼的前提下,推測這個event listener函數會被某個第三方收集暫存起來。
由於「onClick」是react element的一個prop,而這種事件監聽的prop只有寫在reactDOMComponent身上纔有用,因此咱們不妨去reactDOMComponent相關的代碼裏面看看。左瞧右瞧,咱們在reactDOMComponent.js的_createOpenTagMarkup方法裏面看到這樣的一行代碼:
_createOpenTagMarkup: function() { // ...... if (registrationNames[propKey]) { putListener(this._rootNodeID, propKey, propValue); } // ..... 複製代碼
registrationNames是一個怎樣的存在呢?通過追溯,咱們發現它就是當前瀏覽器所支持的事件名改成小駝峯後,再加上「on」爲前綴的事件名的集合。打印出來,是這樣的:
對的,putListener就是react收集咱們event listener的入口。在繼續往下追查以前,咱們不妨自問一下:「react何時開始收集咱們的event listener呢?」。這個問題就是轉換爲:「_createOpenTagMarkup方法何時會被調用呢?」。答曰:「每一個reactDOMComponent在首次掛載的時候,都會調用_createOpenTagMarkup方法」。也就是說,每一個組件在初次掛載以前,都會先收集用戶註冊的event listener。好,咱們明白了收集的時機,接下來,咱們就是要弄清楚,react是如何收集的問題了。在一番代碼導航的操做下,咱們最終到達了咱們的目的地:CallbackRegistry.js的putListener方法:
/* * @param {string} id ID of the DOM element. * @param {string} registrationName Name of listener (e.g. `onClick`). * @param {?function} listener The callback to store. */ putListener: function(id, registrationName, listener) { var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[id] = listener; } 複製代碼
慢着,listenerBank這個變量是怎麼回事呢?眼光往上移動,咱們會看到:
var listenerBank = {};
複製代碼
對,這是CallbackRegistry模塊內的全局變量。它就是react幫咱們儲存event listener的地方。源碼註釋也說得很清楚:
/** * Stores "listeners" by `registrationName`/`id`. There should be at most one * "listener" per `registrationName`/`id` in the `listenerBank`. * * Access listeners via `listenerBank[registrationName][id]`. */ 複製代碼
說得如此直白,我在這裏也不囉裏八嗦了。存儲event listener後的listenerBank的數據結構是形如這樣的:
listenerBank = { onClick: { '[0].[1]': listener // listener就是咱們掛載在jsx的事件回調 } 複製代碼
爲了加深對listenerBank數據結構的印象,咱們把實際應用中listenerBank打印出來的:
相似於'[0].[1]'這種字符串是一個reactid值(在後期版本中,reactid會被去掉?),對應着頁面上一個由react渲染出來的真實DOM元素。
通過上面的一些細節分析,咱們能夠把event listener的收集過程總結以下:
到這裏,react合成事件系統的第二階段算是講完了,咱們只須要記住listenerBank對象的數據結構就好,以便於在第三階段講解涉及取回event listener時能有很好的理解。
其實,相比event listener的調用階段(也就是第三階段),上面提到的一,二階段均可以算做準備工做。由於,到目前爲止,咱們的event listener還乖乖地躺在listenerBank的懷抱裏面沉睡呢。
重頭戲終於來了。從event listener的函數簽名void func(event)
能夠得知咱們第四階段有如下的兩個探索點:
事實上,調用階段的入口函數handleTopLevel正是幹了這兩件事情:
/** * Streams a fired top-level event to `EventPluginHub` where plugins have the * opportunity to create `ReactEvent`s to be dispatched. * * @param {string} topLevelType Record from `EventConstants`. * @param {object} topLevelTarget The listening component root node. // 這個節點其實就是你點擊的那個元素,即event target。 * @param {string} topLevelTargetID ID of `topLevelTarget`. * @param {object} nativeEvent Native environment event. */ handleTopLevel: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { // 1. 合成event object var events = EventPluginHub.extractEvents( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent ); // 2. 調用event listenter ReactUpdates.batchedUpdates(runEventQueueInBatch, events); } 複製代碼
其中「合成event object」又分兩步走:
全部的eventPlugin核心實現的就是上面兩個功能需求。咱們不妨抽取兩三個plugin來看看。
extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { return null; } var EventConstructor; switch(topLevelType) { case topLevelTypes.topInput: case topLevelTypes.topSubmit: // HTML Events // @see http://www.w3.org/TR/html5/index.html#events-0 EventConstructor = SyntheticEvent; break; case topLevelTypes.topKeyDown: case topLevelTypes.topKeyPress: case topLevelTypes.topKeyUp: EventConstructor = SyntheticKeyboardEvent; break; case topLevelTypes.topBlur: case topLevelTypes.topFocus: EventConstructor = SyntheticFocusEvent; break; case topLevelTypes.topClick: // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return null; } /* falls through */ case topLevelTypes.topContextMenu: case topLevelTypes.topDoubleClick: case topLevelTypes.topDrag: case topLevelTypes.topDragEnd: case topLevelTypes.topDragEnter: case topLevelTypes.topDragExit: case topLevelTypes.topDragLeave: case topLevelTypes.topDragOver: case topLevelTypes.topDragStart: case topLevelTypes.topDrop: case topLevelTypes.topMouseDown: case topLevelTypes.topMouseMove: case topLevelTypes.topMouseUp: EventConstructor = SyntheticMouseEvent; break; case topLevelTypes.topTouchCancel: case topLevelTypes.topTouchEnd: case topLevelTypes.topTouchMove: case topLevelTypes.topTouchStart: EventConstructor = SyntheticTouchEvent; break; case topLevelTypes.topScroll: EventConstructor = SyntheticUIEvent; break; case topLevelTypes.topWheel: EventConstructor = SyntheticWheelEvent; break; case topLevelTypes.topCopy: case topLevelTypes.topCut: case topLevelTypes.topPaste: EventConstructor = SyntheticClipboardEvent; break; } ("production" !== process.env.NODE_ENV ? invariant( EventConstructor, 'SimpleEventPlugin: Unhandled event type, `%s`.', topLevelType ) : invariant(EventConstructor)); var event = EventConstructor.getPooled( dispatchConfig, topLevelTargetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 複製代碼
咱們把目光放在倒數三行代碼便可:
var event = EventConstructor.getPooled( dispatchConfig, topLevelTargetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; 複製代碼
如上,在extractEvents方法中,被省略掉的代碼總的來講就幹了這麼一件事:根據topLevelTypes的值,計算出對應的合成事件對象的構造函數。接下來,如咱們所見,EventConstructor.getPooled()
調用返回一個實例--合成事件對象。實例化沒有使用new 操做符而是普通的函數調用?這是由於這裏使用對象複用(pooling)的技術。關於pooling,上面提到過,其中的技術細節就不展開說了。而倒數第二行的一個函數調用:EventPropagators.accumulateTwoPhaseDispatches(event);
就是要完成第二步驟要作的事情。這個函數調用會產生一個較短的函數調用棧,以下:
getListener() listenerAtPhase() accumulateDirectionalDispatches() traverseParentPath() traverseTwoPhase() accumulateTwoPhaseDispatchesSingle() forEachAccumulated() accumulateTwoPhaseDispatches() 複製代碼
這個調用棧是「合成event object」的關鍵部分,等咱們抽樣觀察完剩下的eventPlugin再回過頭來好好分析。下面,咱們繼續抽樣。
extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { switch (topLevelType) { // Track the input node that has focus. case topLevelTypes.topFocus: if (isTextInputElement(topLevelTarget) || topLevelTarget.contentEditable === 'true') { activeElement = topLevelTarget; activeElementID = topLevelTargetID; lastSelection = null; } break; case topLevelTypes.topBlur: activeElement = null; activeElementID = null; lastSelection = null; break; // Do not fire the event while the user is dragging. This matches the // semantics of the native select event. case topLevelTypes.topMouseDown: mouseDown = true; break; case topLevelTypes.topContextMenu: case topLevelTypes.topMouseUp: mouseDown = false; return constructSelectEvent(nativeEvent); // Chrome and IE fire non-standard event when selection is changed (and // sometimes when it has not). case topLevelTypes.topSelectionChange: return constructSelectEvent(nativeEvent); // Firefox does not support selectionchange, so check selection status // after each key entry. case topLevelTypes.topKeyDown: if (!useSelectionChange) { activeNativeEvent = nativeEvent; setTimeout(dispatchDeferredSelectEvent, 0); } break; } } 複製代碼
而constructSelectEvent的實現是這樣的:
function constructSelectEvent(nativeEvent) { // Ensure we have the right element, and that the user is not dragging a // selection (this matches native `select` event behavior). if (mouseDown || activeElement != getActiveElement()) { return; } // Only fire when selection has actually changed. var currentSelection = getSelection(activeElement); if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) { lastSelection = currentSelection; var syntheticEvent = SyntheticEvent.getPooled( eventTypes.select, activeElementID, nativeEvent ); syntheticEvent.type = 'select'; syntheticEvent.target = activeElement; EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent); return syntheticEvent; } } 複製代碼
仔細看,咱們又看到一個相同的代碼「範式」了:
var syntheticEvent = SyntheticEvent.getPooled( eventTypes.select, activeElementID, nativeEvent ); syntheticEvent.type = 'select'; syntheticEvent.target = activeElement; EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent); return syntheticEvent; 複製代碼
嗯嗯,就是SyntheticEvent.getPooled()
和EventPropagators.accumulateTwoPhaseDispatches()
;
extractEvents: function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { var getTargetIDFunc, handleEventFunc; if (shouldUseChangeEvent(topLevelTarget)) { if (doesChangeEventBubble) { getTargetIDFunc = getTargetIDForChangeEvent; } else { handleEventFunc = handleEventsForChangeEventIE8; } } else if (isTextInputElement(topLevelTarget)) { if (isInputEventSupported) { getTargetIDFunc = getTargetIDForInputEvent; } else { getTargetIDFunc = getTargetIDForInputEventIE; handleEventFunc = handleEventsForInputEventIE; } } else if (shouldUseClickEvent(topLevelTarget)) { getTargetIDFunc = getTargetIDForClickEvent; } if (getTargetIDFunc) { var targetID = getTargetIDFunc( topLevelType, topLevelTarget, topLevelTargetID ); if (targetID) { var event = SyntheticEvent.getPooled( eventTypes.change, targetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } } if (handleEventFunc) { handleEventFunc( topLevelType, topLevelTarget, topLevelTargetID ); } } 複製代碼
是的,again:
if (targetID) { var event = SyntheticEvent.getPooled( eventTypes.change, targetID, nativeEvent ); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } 複製代碼
抽樣完畢。正如咱們上面所下的結論那樣,全部的eventPlugin所要實現的核心兩個功能需求就是:
由於,實例化合成事件對象這個過程包含了不少實現細節,好比應對瀏覽器差別而作的兼容細節,pooling技術等等。同時,不一樣的事件類型所要作的瀏覽器兼容不盡相同,不一樣的事件類型的構造函數實現方式也不盡相同。這裏裏面包含太多的細節了,跟咱們的主線沒有太密切的關係,故不深刻探究了。感興趣的同窗,可另行研究。這裏把重點放在第二步將要調用的event listener保存在這個對象上。說白一點,就是說,咱們要研究的就是上面在分析SimpleEventPlugin時所提到函數調用棧道:
getListener() // 棧頂 listenerAtPhase() accumulateDirectionalDispatches() traverseParentPath() traverseTwoPhase() accumulateTwoPhaseDispatchesSingle() forEachAccumulated() accumulateTwoPhaseDispatches() // 棧底 複製代碼
從這個調用棧頂部的getListener()方法名得知,咱們的研究方向是沒錯了。由於不管如具體實現如何,去收集event listener的這個動做都應該發生的。那麼,接下來,咱們帶着「調用階段,event listener的收集過程是如何進行的呢?」這個疑問繼續探索下去。
首先,咱們看看accumulateTwoPhaseDispatches函數的簽名:void func(events)
。從函數簽名,咱們能夠看到參數叫events。從調試結果來看,這個events的數據類型能夠是單個event object,也能夠是多個event object組成的數組。大多數狀況下,咱們看到都是單個event object。而什麼狀況下是數組呢?這個目前我還沒研究出來,擇日研究吧。從accumulateTwoPhaseDispatches()這個方法名,咱們能夠得知,這個過程就是在各個傳入的event object身上去累積(accumulate)event listenter的過程。又由於在這裏,咱們討論的events實參是單個object的狀況,因此,forEachAccumulated()方法就形同虛設了。爲何這麼說呢?看它的代碼是實現就知道:
/** * @param {array} an "accumulation" of items which is either an Array or * a single item. Useful when paired with the `accumulate` module. This is a * simple utility that allows us to reason about a collection of items, but * handling the case when there is exactly one item (and we do not need to * allocate an array). */ var forEachAccumulated = function forEachAccumulated(arr, cb, scope) { if (Array.isArray(arr)) { arr.forEach(cb, scope); } else if (arr) { cb.call(scope, arr); } }; 複製代碼
在咱們討論的狀況中,最終代碼會執行到else if
分支。也就是會說,最終結果會來到accumulateTwoPhaseDispatchesSingle(event)這個方法調用:
/** * Collect dispatches (must be entirely collected before dispatching - see unit * tests). Lazily allocate the array to conserve memory. We must loop through * each event and perform the traversal for each one. We can not perform a * single traversal for the entire collection of events because each event may * have a different target. * 方法名中的「single」指的是每個event object */ function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { injection.InstanceHandle.traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event); } } 複製代碼
所謂的「累積event listener(accumulated dispatches)」說白一點就是在實例化後的event object身上開闢了兩個字段:「_dispatchIDs」 和 「_dispatchListeners」,分別用於保存須要被分發event object的DOM節點的reactId和它上面所註冊的event listener。這個累積過程就是從觸發事件的event target開始,遍歷它的捕獲階段和冒泡階段,去收集相關的reactId和event listener。這就是方法名中的「two phase」的意思了。至於方法名中的「single」指的events裏面的「each single event object」了。注意,event object裏面有個dispatchMarker字段,這個字段就是event target身上的reactId。
接下來的,進入的traverseTwoPhase(targetID, cb, arg)方法負責的正是真真正正的遍歷:
/** * Simulates the traversal of a two-phase, capture/bubble event dispatch. * * NOTE: This traversal happens on IDs without touching the DOM. * * @param {string} targetID ID of the target node. * @param {function} cb Callback to invoke. * @param {*} arg Argument to invoke the callback with. * @internal */ traverseTwoPhase: function traverseTwoPhase(targetID, cb, arg) { // console.log('targetID:', targetID); if (targetID) { traverseParentPath('', targetID, cb, arg, true, false); traverseParentPath(targetID, '', cb, arg, false, true); } }, 複製代碼
若是往前去探究traverseParentPath方法的簽名void function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast)
,咱們就是發現,if條件分支裏的第一行語句traverseParentPath('', targetID, cb, arg, true, false);
指的就是遍歷event target事件傳播的捕獲階段;而第二行語句traverseParentPath(targetID, '', cb, arg, false, true); }
則是指遍歷event target事件傳播的冒泡階段(傳參的那個空的字符串表明這event target層級關係中最遠的祖先元素的父節點)。注意,一個完整的事件傳播中,先進行捕獲階段,再進行冒泡階段,這二者的前後順序就是由這兩行代碼的前後順序所決定的。不信?那咱們就來驗證如下。咱們在同一個元素上同時註冊了冒泡事件和捕獲事件,在event listener裏面打個log,結果以下:
如你所見,這個順序已經改變了。這也證實了個人結論是正確的了。好了,接下來輪到 traverseParentPath方法來作切實的for循環。咱們來看看它的源碼:
/** * Traverses the parent path between two IDs (either up or down). The IDs must * not be the same, and there must exist a parent path between them. * * @param {?string} start ID at which to start traversal. * @param {?string} stop ID at which to end traversal. * @param {function} cb Callback to invoke each ID with. * @param {?boolean} skipFirst Whether or not to skip the first node. * @param {?boolean} skipLast Whether or not to skip the last node. * @private */ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) { start = start || ''; stop = stop || ''; "production" !== process.env.NODE_ENV ? invariant(start !== stop, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : invariant(start !== stop); var traverseUp = isAncestorIDOf(stop, start); "production" !== process.env.NODE_ENV ? invariant(traverseUp || isAncestorIDOf(start, stop), 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(traverseUp || isAncestorIDOf(start, stop)); // Traverse from `start` to `stop` one depth at a time. var depth = 0; var traverse = traverseUp ? getParentID : getNextDescendantID; for (var id = start;; /* until break */id = traverse(id, stop)) { if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { cb(id, traverseUp, arg); } if (id === stop) { // Only break //after// visiting `stop`. break; } "production" !== process.env.NODE_ENV ? invariant(depth++ < MAX_TREE_DEPTH, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop) : invariant(depth++ < MAX_TREE_DEPTH); } } 複製代碼
看到for循環了嗎?對,就是在for循環裏面,一個個地把event listener入隊到event._dispatchListeners數組裏面的。for循環裏面的第一個if其實能夠轉換爲:
if(!((skipFirst && id === start) || (skipLast && id === stop))) { cb(id, traverseUp, arg); } 複製代碼
也便是除了傳播路徑上 最遠祖先元素的父節點以外,其餘節點的event listener都是要收集的。這裏面cb(id, traverseUp, arg);
就是指accumulateDirectionalDispatches(domID, upwards, event)
。在遍歷過程當中,就是這個方法負責根據domID和所處的階段(向上遍歷就是處在冒泡階段;向下遍歷就是處在捕獲階段)來查找到對應的event listener,而後就該event listener入隊到event._dispatchListeners中去。咱們來看看這裏的源碼:
/** * Tags a `SyntheticEvent` with dispatched listeners. Creating this function * here, allows us to not have to bind or create functions for each event. * Mutating the event members allows us to not have to create a wrapping * "dispatch" object that pairs the event with the listener. */ function accumulateDirectionalDispatches(domID, upwards, event) { if ("production" !== process.env.NODE_ENV) { if (!domID) { throw new Error('Dispatching id must not be null'); } injection.validate(); } var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; var listener = listenerAtPhase(domID, event, phase); if (listener) { event._dispatchListeners = accumulate(event._dispatchListeners, listener); event._dispatchIDs = accumulate(event._dispatchIDs, domID); } } 複製代碼
var listener = listenerAtPhase(domID, event, phase);
複製代碼
if (listener) { event._dispatchListeners = accumulate(event._dispatchListeners, listener); event._dispatchIDs = accumulate(event._dispatchIDs, domID); } 複製代碼
而listenerAtPhase(domID, event, phase)最終調用getListener方法,根據domID(實質就是指reactId)和階段性事件註冊名(好比冒泡階段:onClick;捕獲階段:onClickCapture)去咱們在第一個階段所提到的listenerBank這個事件註冊登記表裏面查找event listener。若是又找到event listener,就將其入隊。入隊操做是由accumulate()方法完成,本質上就是一個數組的concat。
說到這裏,咱們基本上把這個「實例化合成事件對象」這個步驟所涉及的流程梳理清楚了。在這個過程所對應的函數調用棧中,最重要的就是traverseTwoPhase這個函數的調用了。就是在這個函數以上的調用棧中,react保證了event listener入隊的兩個順序。哪兩個順序呢?第一個是註冊在捕獲階段的event listener要先於冒泡階段的event listener入隊;第二個是註冊在各個事件傳播階段的event listener的入隊順序要正確。
關於第一個順序的保證,上面已經說起過。那就是經過如下兩個語句的前後順序來保證:
traverseParentPath('', targetID, cb, arg, true, false); traverseParentPath(targetID, '', cb, arg, false, true); 複製代碼
第二個順序的保證就是for循環中,經過沿着給定event target的層級關係鏈向上或向下,逐一遍歷,逐一入隊來保證的。具體就是經過如下代碼來實現:
var traverse = traverseUp ? getParentID : getNextDescendantID; for (var id = start;; /* until break */id = traverse(id, stop)) { if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { cb(id, traverseUp, arg); } if (id === stop) { // Only break //after// visiting `stop`. break; } ) 複製代碼
到目前爲止,咱們須要調用的event listener已經妥妥地保存在event._dispatchListeners數組裏了。一切等待react的調用。那麼,下面咱們就來說述第二步驟:「調用event listenter」。
調用流程的入口是下面的這個代碼:
ReactUpdates.batchedUpdates(runEventQueueInBatch, events);
複製代碼
對應的函數調用棧是:
調用流程發生一個transaction(事務)裏面。transaction模式相似於一個wrapper,主要做用其實就是調用一個核心方法。由於本文是深刻react合成事件系統,因此,我不打算闡述transaction的模式與原理,咱們只須要知道,跟event listener調用過程相關的這個核心方法是「runEventQueueInBatch 」方法便可。不講transaction模式的話,那麼event listener調用過程就比較簡單了,能夠總結爲:兩個變量,兩次循環。
哪兩個變量呢?答曰:
eventQueue和event._dispatchListeners都是隊列(在javascript中,用數組來實現)。eventQueue在上面提過,當它是數組的時候,那麼該數組就是由event object(SyntheticEvent實例)組成的。而event object的_dispatchListeners這個數組又是由咱們的event listener組成。在調用棧中,咱們能夠找到這兩個負責作循環的方法:
eventQueue |
|--- event1
|--- event2
|--- ......
|--- eventn._dispatchListeners|
|--- listener1
|--- listener1
|--- .........
|--- listenern
複製代碼
兩個的關係就是如上圖示。因此,不難理解,要想調用event listener,則須要通過兩次循環(相似於二位數組的雙重循環)。從方法名不難看出,調用棧中負責這兩次循環的方法是:
通常狀況下,eventQueue只有event object,因此,forEachAccumulated(arr, cb, scope)沒什麼好講的。由於forEachEventDispatch(event, cb)這個循環中有一個很重要的實現,那就是「阻止事件傳播」的事件機制實現。下面,咱們重點看看這個方法的實現代碼:
/** * Invokes `cb(event, listener, id)`. Avoids using call if no scope is * provided. The `(listener,id)` pair effectively forms the "dispatch" but are * kept separate to conserve memory. */ function forEachEventDispatch(event, cb) { var dispatchListeners = event._dispatchListeners; var dispatchIDs = event._dispatchIDs; if ("production" !== process.env.NODE_ENV) { validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { if (event.isPropagationStopped()) { break; } cb(event, dispatchListeners[i], dispatchIDs[i]); } } else if (dispatchListeners) { cb(event, dispatchListeners, dispatchIDs); } } 複製代碼
一個大大的for循環映入眼簾,相信你也看到了。在for循環裏面,cb(event, dispatchListeners[i], dispatchIDs[i]);
實質上就是負責真正地調用(使用調用操做符)event listener的executeDispatch(event, dispatchListeners[i], dispatchIDs[i])
,而一個平淡無奇的「break」關鍵字倒是實現「阻止事件傳播」的事件機制的靈魂之所在。當event object 的isPropagationStopped方法返回值爲true的時候,「break」一下,咱們跳出了整個大循環,從而也就不執行隊列後面的全部event listener了,從而實現了「阻止事件傳播」的事件機制。那何時isPropagationStopped方法的返回值是true呢?咱們不妨全局搜索一下,看看它的實現代碼(在SyntheticEvent.js):
stopPropagation: function() { var event = this.nativeEvent; event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; this.isPropagationStopped = emptyFunction.thatReturnsTrue; }, 複製代碼
從代碼中,咱們能夠看出,當隊列中前一個event listener中,用戶手動調用了這個stopPropagation方法的時候,react就會在event object身上追加一個字段,它的值是一個函數引用,一個返回true值的函數引用。所以,當for循環執行到下一個循環的時候,isPropagationStopped就指向emptyFunction.thatReturnsTrue,if條件就爲真,因而跳出整個大循環。
好,在react合成事件系統中,「阻止事件傳播」的事件機制是如何實現的,已經講完了。下面咱們繼續往下看。
上面咱們也提到,真正負責調用(使用調用操做符)event listener的方法是executeDispatch(event, dispatchListeners[i], dispatchIDs[i])
。這個executeDispatch方法實際上是一個函數引用。它具體所指能夠由如下代碼能夠看出
/** * Dispatches an event and releases it back into the pool, unless persistent. * * @param {?object} event Synthetic event to be dispatched. * @private */ var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) { if (event) { var executeDispatch = EventPluginUtils.executeDispatch; // Plugins can provide custom behavior when dispatching events. var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event); if (PluginModule && PluginModule.executeDispatch) { executeDispatch = PluginModule.executeDispatch; } EventPluginUtils.executeDispatchesInOrder(event, executeDispatch); if (!event.isPersistent()) { event.constructor.release(event); } } }; 複製代碼
結合上面的註釋「Plugins can provide custom behavior when dispatching events.」和在EventPluginHub.js裏面對EventPluginHub的註釋:
/** * This is a unified interface for event plugins to be installed and configured. * * Event plugins can implement the following properties: * * `extractEvents` {function(string, DOMEventTarget, string, object): *} * Required. When a top-level event is fired, this method is expected to * extract synthetic events that will in turn be queued and dispatched. * * `eventTypes` {object} * Optional, plugins that fire events must publish a mapping of registration * names that are used to register listeners. Values of this mapping must * be objects that contain `registrationName` or `phasedRegistrationNames`. * * `executeDispatch` {function(object, function, string)} * Optional, allows plugins to override how an event gets dispatched. By * default, the listener is simply invoked. * * Each plugin that is injected into `EventsPluginHub` is immediately operable. * * @public */ var EventPluginHub = { // ...... } 複製代碼
咱們能夠看出,最後的executeDispatch引用的計算規則是這樣的:若是某某 eventPlugin實現了這個方法,則首先使用它。不然,就使用默認的方法。默認的executeDispatch是怎樣的呢?在原文件EventPluginUtils.js裏面,咱們找到了它:
/** * Default implementation of PluginModule.executeDispatch(). * @param {SyntheticEvent} SyntheticEvent to handle * @param {function} Application-level callback * @param {string} domID DOM id to pass to the callback. */ function executeDispatch(event, listener, domID) { listener(event, domID); } 複製代碼
可見,默認的executeDispatch的實現是最簡單的,也就是說使用函數調用操做符去操做咱們的event listener。
縱觀全部的eventPlugin,好像只有SimpleEventPlugin實現了本身的executeDispatch方法:
/** * Same as the default implementation, except cancels the event when return * value is false. * * @param {object} Event to be dispatched. * @param {function} Application-level callback. * @param {string} domID DOM ID to pass to the callback. */ executeDispatch: function(event, listener, domID) { var returnValue = listener(event, domID); if (returnValue === false) { event.stopPropagation(); event.preventDefault(); } }, 複製代碼
由於SimpleEventPlugin處理了大部分的事件類型,因此,通常狀況下,上面提到的那個引用指向的就是SimpleEventPlugin的executeDispatch方法。
咱們目光放在if條件語句中:
if (returnValue === false) { event.stopPropagation(); event.preventDefault(); } 複製代碼
聯繫這段代碼的上下文,咱們能夠得知,咱們平日react開發過程當中,經過在event listener返回false來阻止事件傳播和取消默認行爲就是經過這段代碼來實現的。從這段代碼,咱們也知道,在event listener中返回false,就是至關於react幫咱們在event object身上調用了stopPropagation方法。因此,咱們能夠有如下結論:在react應用中,若是你想阻止事件傳播,你有兩種方式:
現在,咱們已經明明白白地看到了對event listener的調用了:
listener(event, domID)
複製代碼
從以上代碼,咱們能夠看出,在reactV0.8.0中,咱們的event listener實際上是被傳入兩個實參的,只不過當時第二個參數reactId不多人用罷了。
說到這裏,咱們已經梳理到調用event listener流程的末端了,也就是說,第三階段的總體分析也完成了。整個第三階段有如下的幾個研究重點,下面回顧一下:
收尾階段主要是對eventQueue和event object(當前event loop dispatch的那個)所佔據的內存進行釋放。在javascript中,釋放內存無非就是把某個變量賦值爲null。
首先,咱們看看eventQueue的內存釋放(在EventPluginHub.js中):
processEventQueue: function() { // Set `eventQueue` to null before processing it so that we can tell if more // events get enqueued while processing. var processingEventQueue = eventQueue; eventQueue = null; forEachAccumulated(processingEventQueue, executeDispatchesAndRelease); ("production" !== process.env.NODE_ENV ? invariant( !eventQueue, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.' ) : invariant(!eventQueue)); } 複製代碼
而後,咱們來看看event object的內存釋放。
第一步,執行完全部的event listener後,清空一下_dispatchListeners和_dispatchIDs這兩個隊列:
/** * Standard/simple iteration through an event s collected dispatches。 * */ function executeDispatchesInOrder(event, executeDispatch) { forEachEventDispatch(event, executeDispatch); event._dispatchListeners = null; event._dispatchIDs = null; } 複製代碼
第二步,結合pooling技術作內存釋放:
var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) { if (event) { var executeDispatch = EventPluginUtils.executeDispatch; // Plugins can provide custom behavior when dispatching events. var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event); if (PluginModule && PluginModule.executeDispatch) { executeDispatch = PluginModule.executeDispatch; } EventPluginUtils.executeDispatchesInOrder(event, executeDispatch); if (!event.isPersistent()) { event.constructor.release(event); } } }; 複製代碼
咱們能夠看到,若是用戶沒有手動去持久化(event.isPersistent=function(){ return true})這個event object的話,那麼這個event object就會被釋放掉(release)。怎麼釋放呢?咱們拿當前的event object是SyntheticMouseEvent的實例的這種狀況舉個例子,那麼event.constructor就是指SyntheticMouseEvent類:
function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent) { SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent); } 複製代碼
從上面能夠看出,SyntheticMouseEvent實質上是繼承SyntheticUIEvent的,而SyntheticUIEvent又是繼承SyntheticEvent。咱們覺得會在SyntheticEvent的代碼裏面找到了release方法的實現代碼,實際上是在PooledClass.js裏面找到的。爲何呢?由於release方法是react把SyntheticEvent加入到pooling池後動態添加的靜態方法:
PooledClass.addPoolingTo(SyntheticEvent, PooledClass.threeArgumentPooler);
複製代碼
而addPoolingTo方法的實現代碼是這樣的:
/** * Augments `CopyConstructor` to be a poolable class, augmenting only the class * itself (statically) not adding any prototypical fields. Any CopyConstructor * you give this may have a `poolSize` property, and will look for a * prototypical `destructor` on instances (optional). * * @param {Function} CopyConstructor Constructor that can be used to reset. * @param {Function} pooler Customizable pooler. */ var addPoolingTo = function(CopyConstructor, pooler) { var NewKlass = CopyConstructor; NewKlass.instancePool = []; NewKlass.getPooled = pooler || DEFAULT_POOLER; if (!NewKlass.poolSize) { NewKlass.poolSize = DEFAULT_POOL_SIZE; } NewKlass.release = standardReleaser; return NewKlass; }; 複製代碼
看到了沒? NewKlass.release = standardReleaser;
語句中的NewKlass就是指SyntheticEvent。因此,到最後,event.constructor.release
的release指向的是standardReleaser。因而,咱們來看看standardReleaser是什麼樣的呢:
var standardReleaser = function(instance) { var Klass = this; if (instance.destructor) { instance.destructor(); } if (Klass.instancePool.length < Klass.poolSize) { Klass.instancePool.push(instance); } }; 複製代碼
參數instance在實參階段就是SyntheticMouseEvent實例。因而,咱們沿着SyntheticMouseEvent實例的原型鏈上查找一下這個destructor方法,終於找他它了(在SyntheticEvent.js中):
/** * `PooledClass` looks for `destructor` on each instance it releases. */ destructor: function() { var Interface = this.constructor.Interface; for (var propName in Interface) { this[propName] = null; } this.dispatchConfig = null; this.dispatchMarker = null; this.nativeEvent = null; } 複製代碼
咱們能夠看到,對event object的內存釋放工做,主要是把它的各個字段所引用的內存所釋放,而並無對它自己所佔據的內存進行釋放。event object最終是被pooling技術所管理的,也就是說,它最終會被回收到實例池中,見standardReleaser方法中的下面代碼片斷:
if (Klass.instancePool.length < Klass.poolSize) { Klass.instancePool.push(instance); } 複製代碼
說到這裏,收尾階段已經分析完了。下面再說多一點。那就是在下一次event loop開始的時候,react是如何從實例池中取回實例的呢?其實這就銜接回咱們的第三階段的第一步驟:合成event object。由於每個event loop裏面,都要從新執行extractEvent方法去合成一個event object,而extractEvent方法都會從實例池中取回實例的一行代碼,好比:
var event = SyntheticEvent.getPooled(
eventTypes.change,
targetID,
nativeEvent
);
複製代碼
而這個getPooled方法實際上是在代碼初始化階段,把這個類(好比:SyntheticEvent)加入到pooling池中就決定的。也就是咱們上面提到的addPoolingTo方法調用時中傳入的PooledClass.threeArgumentPooler方法。那咱們就來看看PooledClass.threeArgumentPooler這個方法的實現代碼:
var threeArgumentPooler = function(a1, a2, a3) { var Klass = this; if (Klass.instancePool.length) { var instance = Klass.instancePool.pop(); Klass.call(instance, a1, a2, a3); return instance; } else { return new Klass(a1, a2, a3); } }; 複製代碼
看到這裏,咱們心中想要的答案就明朗了。所謂的「getPooled」就是從被pooling化的類的實例池(實例池是一個數組)中pop一個實例對象出來,並從新對它進行初始化而已。也就是下面的兩行代碼:
var instance = Klass.instancePool.pop();
Klass.call(instance, a1, a2, a3);
複製代碼
講到這裏,不知道你明白了沒?對於event object,咱們在event loop的收尾階段把它放回實例池:Klass.instancePool.push(instance);
。在下一個event loop的開始時候又從新把它拿出來:var instance = Klass.instancePool.pop();
。
四個階段的梳理與講解已經完畢了。下面咱們來作個簡單的總結。
經過不斷地明確研究點,而後反覆寫代碼,反覆地去調試和驗證,我收穫了不少深入的認知。正是這些深入的認知,使得我揭開了react合成事件系統的神祕面紗,清晰地看見了它的真實面目。與此同時,我也加深了對原生事件機制的理解。
下面說說個人收穫:
絞盡腦汁,我就總結這麼多了。雖然,本文探索的是reactV0.8.0的合成事件系統,可是我相信即便版本已經更迭到v16.12.0,合成事件系統的主要架構和運行時原理都是沒有多大的變化的。
整片文章下來,我相信大致的流程梳理得也算明朗,可是有一些細節是沒有深刻的。好比說,各個合成事件對象構造函數的實現細節,pooling技術細節,transaction(事務)的技術細節等等。正所謂,書不盡言,但願你們也都去探索探索。若是在閱讀過程發現觀點錯誤,還請不吝指教和勘正。
謝謝閱讀,好走不送。