React源碼分析 - 事件機制
React的事件機制還是很好玩的,其中模擬事件傳遞和利用document委託大部分事件的想法比較有意思。
事件機制流程圖
代碼分析
(代碼僅包含涉及事件參數的部分)
_updateDOMProperties是事件參數處理的入口,只要注意enqueuePutListener這個方法就好了,這是註冊事件的入口函數。registrationNameModules變數保存事件類型和對應的方法的映射的一個對象,如圖:
這些映射的初始化的地方在《React源碼分析 - 組件初次渲染》解釋過了。
_updateDOMProperties: function (lastProps, nextProps, transaction) { var propKey; var styleName; var styleUpdates; for (propKey in lastProps) { if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) { continue; } if (registrationNameModules.hasOwnProperty(propKey)) { if (lastProps[propKey]) { // Only call deleteListener if there was a listener previously or // else willDeleteListener gets called when there wasnt actually a // listener (e.g., onClick={null}) deleteListener(this, propKey); } } } for (propKey in nextProps) { var nextProp = nextProps[propKey]; var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined; if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) { continue; } if (registrationNameModules.hasOwnProperty(propKey)) { // 處理事件參數。 if (nextProp) { enqueuePutListener(this, propKey, nextProp, transaction); // 註冊事件,委託到屬於的document上 } else if (lastProp) { deleteListener(this, propKey); } } }}
enqueuePutListener
- listenTo
- putListener
function enqueuePutListener(inst, registrationName, listener, transaction) { var containerInfo = inst._nativeContainerInfo; var doc = containerInfo._ownerDocument; // 大部分的事件都被到對應的document上 if (!doc) { // ssr // Server rendering. return; } listenTo(registrationName, doc); transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener });}
listenTo是將事件委託到document的方法,大部分事件是委託到document上的。但是因為document上能夠catch的事件類型的限制(Document Object Model Events),不是所有的事件類型都委託到document,少部分是直接委託到元素本身上的。
putListener將對應的類型的事件、事件的目標對象和事件觸發時執行的方法添加到listenerBank對象中。
listenTo: function (registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; var isListening = getListeningForDocument(mountAt); var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName]; var topLevelTypes = EventConstants.topLevelTypes; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { // 先判斷先幾個需要特殊處理的事件,主要都是兼容性的原因。 if (...) { ...... } else if (topEventMapping.hasOwnProperty(dependency)) { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt); } isListening[dependency] = true; } }}// 冒泡階段的觸發的事件的委託trapBubbledEvent: function (topLevelType, handlerBaseName, handle) { return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);},// 捕獲階段的觸發的事件的委託trapCapturedEvent: function (topLevelType, handlerBaseName, handle) { return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);},trapBubbledEvent: function (topLevelType, handlerBaseName, handle) { return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));},trapCapturedEvent: function (topLevelType, handlerBaseName, handle) { return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));},listen: function listen(target, eventType, callback) { if (target.addEventListener) { target.addEventListener(eventType, callback, false); }},capture: function capture(target, eventType, callback) { if (target.addEventListener) { target.addEventListener(eventType, callback, true); }},
重點在於所有的委託的事件的回調函數都是ReactEventListener.dispatchEvent。
dispatchEvent: function (topLevelType, nativeEvent) { // bookKeeping的初始化使用了react在源碼中用到的對象池的方法來避免多餘的垃圾回收。 // bookKeeping的作用看ta的定義就知道了,就是一個用來保存過程中會使用到的變數的對象。 var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent); try { ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); }}
handleTopLevelImpl方法遍歷事件觸發對象以及其的父級元素(事件傳遞),對每個元素執行_handleTopLevel方法。
function handleTopLevelImpl(bookKeeping) { var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget); var ancestor = targetInst; do { bookKeeping.ancestors.push(ancestor); ancestor = ancestor && findParent(ancestor); } while (ancestor); for (var i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); }}
handleTopLevel根據事件對象以及觸發的事件類型提取出所有需要被執行的事件以及對應的回調函數,統一由runEventQueueInBatch執行。
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); runEventQueueInBatch(events);}
extractEvents方法調用了對應的plugin的extractEvents方法來獲取對應的plugin類型的需要執行的事件,然後accumulateInto到一起。
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { var events; var plugins = EventPluginRegistry.plugins; for (var i = 0; i < plugins.length; i++) { // Not every plugin in the ordering may be loaded at runtime. var possiblePlugin = plugins[i]; if (possiblePlugin) { var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); if (extractedEvents) { events = accumulateInto(events, extractedEvents); } } } return events; }
plugin的extractEvents方法中的有意思的地方在於 EventPropagators.accumulateTwoPhaseDispatches(event) 。
EventPropagators.accumulateTwoPhaseDispatches中模擬了事件傳遞的過程即:capture -> target -> bubble 的過程,將這個路徑上的所有的符合事件類型的回調函數以及對應的元素按照事件傳遞的順序返回。
(圖片來自Event dispatch and DOM event flow)
function traverseTwoPhase(inst, fn, arg) { var path = []; while (inst) { path.push(inst); inst = inst._nativeParent; } var i; for (i = path.length; i-- > 0;) { fn(path[i], false, arg); } for (i = 0; i < path.length; i++) { fn(path[i], true, arg); }}
traverseTwoPhase方法模擬了事件傳遞的過程並且獲取對應的回調函數和事件對象保存在react合成的event對象的_dispatchListeners和_dispatchInstances上
function accumulateDirectionalDispatches(inst, upwards, event) { var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; var listener = listenerAtPhase(inst, event, phase); if (listener) { // event._dispatchListeners結果就是這個event在event flow的過程中會觸發那些listenter的callback【按照event flow的順序push到一個數組中了】 event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); }}
查詢listener和對應的inst使用的是事件的類型以及_rootNodeID,listenerBank中保存了對應一個類型下元素的回調函數:
function listenerAtPhase(inst, event, propagationPhase) { var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(inst, registrationName);}getListener: function (inst, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; return bankForRegistrationName && bankForRegistrationName[inst._rootNodeID]; },
對於listenerBank內容的生成由之前說的第二個主要方法putListener完成。
putListener 使用事務的方式統一在ReactMountReady階段執行。
putListener: function (inst, registrationName, listener) { var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[inst._rootNodeID] = listener;}
在extractEvents了對應觸發的事件類型的events後通過runEventQueueInBatch(events)將所有的合成事件放到事件隊列裡面,第二步是逐個執行
function runEventQueueInBatch(events) { EventPluginHub.enqueueEvents(events); EventPluginHub.processEventQueue(false);}function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; if (process.env.NODE_ENV !== production) { validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { if (event.isPropagationStopped()) { break; } // Listeners and Instances are two parallel arrays that are always in sync. executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); } } else if (dispatchListeners) { executeDispatch(event, simulated, dispatchListeners, dispatchInstances); } event._dispatchListeners = null; event._dispatchInstances = null;}function executeDispatch(event, simulated, listener, inst) { var type = event.type || unknown-event; event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null;}function invokeGuardedCallback(name, func, a, b) { try { return func(a, b); } catch (x) { if (caughtError === null) { caughtError = x; } return undefined; }}
總結
- 統一的分發函數dispatchEvent。
- React的事件對象是合成對象(SyntheticEvent)。
- 幾乎所有的事件都委託到document,達到性能優化的目的。
- 合成事件與原生事件混用要注意React的事件基本都是委託到document。
參考資料
- React源碼解讀系列 – 事件機制
- UI Events
- Document Object Model Events
推薦閱讀:
※把網頁導出為圖片的兩種方案以及其適用場景
※我理解的同步載入與非同步載入
※2018-02-01第一節課
※序章 曦粥簡言
TAG:前端開發 |