文章原地址:前往閱讀html
本文首先分析React在DOM事件上的架構設計、相關優化、合成事件(Synethic event)對象,從源碼層面上作到庖丁解牛的效果。同時,簡單介紹下react事件可能會遇到的問題。node
react在事件處理上具備以下優勢:react
幾乎全部的事件代理(delegate)到document
,達到性能優化的目的git
對於每種類型的事件,擁有統一的分發函數dispatchEvent
github
事件對象(event)是合成對象(SyntheticEvent),不是原生的性能優化
react內部事件系統實現能夠分爲兩個階段: 事件註冊、事件觸發。架構
ReactDOMComponent在進行組件加載(mountComponent)、更新(updateComponent)的時候,須要對props
進行處理(_updateDOMProperties):dom
ReactDOMComponent.Mixin = { _updateDOMProperties: function (lastProps, nextProps, transaction) { ... for (propKey in nextProps) { // 判斷是否爲事件屬性 if (registrationNameModules.hasOwnProperty(propKey)) { enqueuePutListener(this, propKey, nextProp, transaction); } } } } function enqueuePutListener(inst, registrationName, listener, transaction) { ... var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; listenTo(registrationName, doc); transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); function putListener() { var listenerToPut = this; EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); } }
代碼解析:函數
在props渲染的時候,如何屬性是事件屬性,則會用enqueuePutListener
進行事件註冊性能
上述transaction
是ReactUpdates.ReactReconcileTransaction的實例化對象
enqueuePutListener進行兩件事情: 在document
上註冊相關的事件;對事件進行存儲
document的事件註冊入口位於ReactBrowserEventEmitter
:
// ReactBrowserEventEmitter.js listenTo: function (registrationName, contentDocumentHandle) { ... if (...) { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...); } else if (...) { ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...); } ... } // ReactEventListener.js var ReactEventListener = { ... trapBubbledEvent: function (topLevelType, handlerBaseName, element) { ... var handler = ReactEventListener.dispatchEvent.bind(null, topLevelType); return EventListener.listen(element, handlerBaseName, handler); }, trapCapturedEvent: function (topLevelType, handlerBaseName, element) { var handler = ReactEventListener.dispatchEvent.bind(null, topLevelType); return EventListener.capture(element, handlerBaseName, handler); } dispatchEvent: function (topLevelType, nativeEvent) { ... ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); ... } } function handleTopLevelImpl(bookKeeping) { ... ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); ... }
代碼解析:
事件的註冊、觸發,具體是在ReactEventListener
中實現的
事件的註冊有兩個方法: 支持冒泡(trapBubbledEvent)、trapCapturedEvent
document無論註冊的是什麼事件,具備統一的回調函數handleTopLevelImpl
document的回調函數中不包含任何的事物處理,只起到事件分發的做用
函數的存儲,在ReactReconcileTransaction
事務的close階段執行:
transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); function putListener() { var listenerToPut = this; EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); }
事件的存儲由EventPluginHub
來進行管理,來看看其中的具體實現:
// var listenerBank = {}; var getDictionaryKey = function (inst) { return '.' + inst._rootNodeID; } var EventPluginHub = { putListener: function (inst, registrationName, listener) { ... var key = getDictionaryKey(inst); var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[key] = listener; ... } }
react中的全部事件的回調函數均存儲在listenerBank
對象裏面,根據事件類型、component對象的_rootNodeID爲兩個key,來存儲對應的回調函數。
事件註冊完以後,就能夠依據事件委託進行事件的執行。由事件註冊能夠知道,幾乎全部的事件均委託到document上,而document上事件的回調函數只有一個: ReactEventListener.dispatchEvent,而後進行相關的分發:
var ReactEventListener = { dispatchEvent: function (topLevelType, nativeEvent) { ... ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); ... } } function handleTopLevelImpl(bookKeeping) { var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget); // 初始化時用ReactEventEmitterMixin注入進來的 ReactEventListener._handleTopLevel(..., nativeEventTarget, targetInst); } // ReactEventEmitterMixin.js var ReactEventEmitterMixin = { handleTopLevel: function (...) { var events = EventPluginHub.extractEvents(...); runEventQueueInBatch(events); } } function runEventQueueInBatch(events) { EventPluginHub.enqueueEvents(events); EventPluginHub.processEventQueue(false); }
代碼解析:
handleTopLevelImpl: 根據原生的事件對象,找到事件觸發的dom元素以及該dom對應的compoennt對象
ReactEventEmitterMixin: 一方面生成合成的事件對象,另外一方面批量執行定義的回調函數
runEventQueueInBatch: 進行批量更新
react中的事件對象不是原生的事件對象,而是通過處理後的對象,下面從源碼層面解析是如何生成的:
// EventPluginHub.js var EventPluginHub = { extractEvents: function (...) { var events; var plugins = EventPluginRegistry.plugins; for (var i = 0; i < plugins.length; i++) { var possiblePlugin = plugins[i]; if (possiblePlugin) { var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); if (extractedEvents) { events = accumulateInto(events, extractedEvents); } } } return events; } }
EventPluginHub不只存儲事件的回調函數,並且還管理其中不一樣的plugins,這些plugins是在系統啓動過程當中注入(injection)過來的:
// react-dom模塊的入口文件ReactDOM.js: var ReactDefaultInjection = require('./ReactDefaultInjection'); ReactDefaultInjection.inject(); ... // ReactDefaultInjection.js module.exports = { inject: inject }; function inject() { ... ReactInjection.EventPluginHub.injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, SelectEventPlugin: SelectEventPlugin, BeforeInputEventPlugin: BeforeInputEventPlugin }); ... }
從上面代碼能夠看到,默認狀況下,react注入了五種事件plugin,針對不一樣的事件,獲得不一樣的合成事件,以最多見的SimpleEventPlugin
爲例進行分析:
var SimpleEventPlugin = { extractEvents: function (topLevelType, ...) { var EventConstructor; switch (topLevelType) { EventConstructor = one of [ SyntheticEvent, SyntheticKeyboardEvent, SyntheticFocusEvent, SyntheticMouseEvent, SyntheticDragEvent, SyntheticTouchEvent, SyntheticAnimationEvent, SyntheticTransitionEvent, SyntheticUIEvent, SyntheticWheelEvent, SyntheticClipboardEvent]; } var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget); EventPropagators.accumulateTwoPhaseDispatches(event); return event; } }
代碼解析:
針對不一樣的事件類型,會生成不一樣的合成事件
EventPropagators.accumulateTwoPhaseDispatches: 用於從EventPluginHub中獲取回調函數,後面小節會具體分析獲取過程
以其中的最基本的SyntheticEvent
爲例進行分析:
function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) { ... this.dispatchConfig = dispatchConfig; this._targetInst = targetInst; this.nativeEvent = nativeEvent; var Interface = this.constructor.Interface; for (var propName in Interface) { var normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { if (propName === 'target') { this.target = nativeEventTarget; } else { this[propName] = nativeEvent[propName]; } } } ... } _assign(SyntheticEvent.prototype, { preventDefault: function () { ... }, stopPropagation: function () { ... }, ... }); var EventInterface = { type: null, target: null, // currentTarget is set when dispatching; no use in copying it here currentTarget: emptyFunction.thatReturnsNull, eventPhase: null, bubbles: null, cancelable: null, timeStamp: function (event) { return event.timeStamp || Date.now(); }, defaultPrevented: null, isTrusted: null }; SyntheticEvent.Interface = EventInterface; // 實現繼承關係 SyntheticEvent.augmentClass = function (Class, Interface) { ... }
上述合成事件對象在生成的過程當中,會從EventPluginHub
處獲取相關的回調函數,具體實現以下:
// EventPropagators.js function accumulateTwoPhaseDispatches(events) { forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); } function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); } } 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); } } var getListener = EventPluginHub.getListener; function listenerAtPhase(inst, event, propagationPhase) { var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(inst, registrationName); } // EventPluginHub.js getListener: function (inst, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; var key = getDictionaryKey(inst); return bankForRegistrationName && bankForRegistrationName[key]; },
react會進行批量處理具體的回調函數,回調函數的執行爲了兩步,第一步是將全部的合成事件放到事件隊列裏面,第二步是逐個執行:
var eventQueue = null; var EventPluginHub = { enqueueEvents: function (events) { if (events) { eventQueue = accumulateInto(eventQueue, events); } }, processEventQueue: function (simulated) { var processingEventQueue = eventQueue; ... forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); ... }, } var executeDispatchesAndReleaseSimulated = function (e) { return executeDispatchesAndRelease(e, true); }; var executeDispatchesAndRelease = function (event, simulated) { if (event) { EventPluginUtils.executeDispatchesInOrder(event, simulated); if (!event.isPersistent()) { event.constructor.release(event); } } }; // EventPluginUtils.js function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; ... executeDispatch(event, simulated, dispatchListeners, dispatchInstances); ... event._dispatchListeners = null; event._dispatchInstances = null; }
在開發過程當中,有時候須要使用到原生事件,例如存在以下的業務場景: 點擊input框展現日曆,點擊文檔其餘部分,日曆消失,代碼以下:
// js部分 var React = require('react'); var ReactDOM = require('react-dom'); class App extends React.Component { constructor(props) { super(props); this.state = { showCalender: false }; } componentDidMount() { document.addEventListener('click', () => { this.setState({showCalender: false}); console.log('it is document') }, false); } render() { return (<div> <input type="text" onClick={(e) => { this.setState({showCalender: true}); console.log('it is button') e.stopPropagation(); }} /> <Calendar isShow={this.state.showCalender}></Calendar> </div>); } }
上述代碼: 在點擊input的時候,state狀態變成true,展現日曆,同時阻止冒泡,可是document上的click事件仍然觸發了?究竟是什麼緣由形成的呢?
緣由解讀: 由於react的事件基本都是委託到document上的,並無真正綁定到input元素上,因此在react中執行stopPropagation並無什麼用處,document上的事件依然會觸發。
解決辦法:
class App extends React.Component { constructor(props) { super(props); this.state = { showCalender: false }; } componentDidMount() { document.addEventListener('click', () => { this.setState({showCalender: false}); console.log('it is document') }, false); this.refs.myBtn.addEventListener('click', (e) => { this.setState({showCalender: true}); e.stopPropagation(); }, false); } render() { return (<div> <input type="text" ref="myBtn" /> <Calendar isShow={this.state.showCalender}></Calendar> </div>); } }
class App extends React.Component { constructor(props) { super(props); this.state = { showCalender: false }; } componentDidMount() { document.addEventListener('click', (e) => { var tar = document.getElementById('myInput'); if (tar.contains(e.target)) return; console.log('document!!!'); this.setState({showCalender: false}); }, false); } render() { return (<div> <input id="myInput" type="text" onClick={(e) => { this.setState({showCalender: true}); console.log('it is button') // e.stopPropagation(); }} /> <Calendar isShow={this.state.showCalender}></Calendar> </div>); } }
React在設計事件機制的時候,利用冒泡原理充分提升事件綁定的效率,使用EventPluginHub
對回調函數、事件插件進行管理,而後經過一個統一的入口函數實現事件的分發,整個設計思考跟jQuery的事件實現上存在類似的地方,很是值得學習借鑑。