對React熟悉的同窗都知道,React中的事件機制並非原生的那一套,事件沒有綁定在原生DOM上,發出的事件也是對原生事件的包裝。
那麼這一切是怎麼實現的呢?node
首先仍是看咱們熟悉的代碼react
<button onClick={this.autoFocus}>點擊聚焦</button>
這是咱們在React中綁定事件的常規寫法。經由JSX解析,button會被當作組件掛載。而onClick
這時候也只是一個普通的props。
ReactDOMComponent在進行組件加載(mountComponent)、更新(updateComponent)的時候,須要對props進行處理(_updateDOMProperties):jquery
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) { ... //注意這裏!!!!!!!!! //這裏獲取了當前組件(其實這時候就是button)所在的document 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); } }
綁定的重點是這裏的listenTo方法。看源碼(ReactBrowerEventEmitter)數組
//registrationName:須要綁定的事件 //當前component所屬的document,即事件須要綁定的位置 listenTo: function (registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; //獲取當前document上已經綁定的事件 var isListening = getListeningForDocument(mountAt); ... if (...) { //冒泡處理 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...); } else if (...) { //捕捉處理 ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...); } ... },
最後處理(EventListener的listen和capture中)瀏覽器
//eventType:事件類型,target: document對象, //callback:是固定的,始終是ReactEventListener的dispatch方法 if (target.addEventListener) { target.addEventListener(eventType, callback, false); return { remove: function remove() { target.removeEventListener(eventType, callback, false); } }; }
從事件註冊的機制中不難看出:函數
看到這邊你可能疑惑,全部回調都執行的ReactEventListener的dispatch方法,那我寫的回調幹嗎去了。別急,接着看:this
function enqueuePutListener(inst, registrationName, listener, transaction) { ... //注意這裏!!!!!!!!! //這裏獲取了當前組件(其實這時候就是button)所在的document var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; //事件綁定 listenTo(registrationName, doc); //這段代碼表示將putListener放入回調序列,當組件掛載完成是會依次執行序列中的回調。putListener也是在那時候執行的。 //不明白的能夠看看本專欄中前兩篇關於transaction和掛載機制的講解 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); //保存回調 function putListener() { var listenerToPut = this; EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); } }
仍是這段代碼,事件綁定咱們介紹過,主要是listenTo
方法。
當綁定完成之後會執行putListener。該方法會在ReactReconcileTransaction事務的close階段執行,具體由EventPluginHub來進行管理spa
// var listenerBank = {}; var getDictionaryKey = function (inst) { //inst爲組建的實例化對象 //_rootNodeID爲組件的惟一標識 return '.' + inst._rootNodeID; } var EventPluginHub = { //inst爲組建的實例化對象 //registrationName爲事件名稱 //listner爲咱們寫的回調函數,也就是列子中的this.autoFocus putListener: function (inst, registrationName, listener) { ... var key = getDictionaryKey(inst); var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[key] = listener; ... } }
EventPluginHub在每一個項目中只實例化一次。也就是說,項目組全部事件的回調都會儲存在惟一的listenerBank中。設計
是否是有點暈,放上流程圖,仔細回憶一下
code
註冊事件時咱們說過,全部的事件都是綁定在Document上。回調統一是ReactEventListener的dispatch方法。
因爲冒泡機制,不管咱們點擊哪一個DOM,最後都是由document響應(由於其餘DOM根本沒有事件監聽)。也便是說都會觸發dispatch
dispatchEvent: function(topLevelType, nativeEvent) { //實際觸發事件的DOM對象 var nativeEventTarget = getEventTarget(nativeEvent); //nativeEventTarget對應的virtual DOM var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode( nativeEventTarget, ); ... //建立bookKeeping實例,爲handleTopLevelImpl回調函數傳遞事件名和原生事件對象 //其實就是把三個參數封裝成一個對象 var bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, ); try { //這裏開啓一個transactIon,perform中執行了 //handleTopLevelImpl(bookKeeping) ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); } },
這裏把節奏放慢點,咱們一步步跟。
function handleTopLevelImpl(bookKeeping) { //觸發事件的真實DOM var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); //nativeEventTarget對應的ReactElement var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget); //bookKeeping.ancestors保存的是組件。 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: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { //首先封裝event事件 var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); //發送包裝好的event runEventQueueInBatch(events); }
首先是EventPluginHub
的extractEvents
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); ...... } } return events; },
接着看SimpleEventPlugin的方法
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { ...... //這裏是對事件的封裝,可是不是咱們關注的重點 var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget); //重點看這邊 EventPropagators.accumulateTwoPhaseDispatches(event); return event; }
接下來是方法中的各類引用,跳啊跳,轉啊轉,咱們來到了ReactDOMTraversal
中的traverseTwoPhase
方法
//inst是觸發事件的target的ReactElement //fn:EventPropagator的accumulateDirectionalDispatches //arg: 就是以前部分封裝好的event(之因此說是部分,是由於如今也是在處理Event,這邊處理完纔是封裝完成) function traverseTwoPhase(inst, fn, arg) { var path = []; while (inst) { //注意path,這裏以ReactElement的形式冒泡着, //把觸發事件的父節點依次保存下來 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); } }
//判斷父組件是否保存了這一類事件 function accumulateDirectionalDispatches(inst, phase, event) { //獲取到回調 var listener = listenerAtPhase(inst, event, phase); if (listener) { //若是有回調,就把包含該類型事件監聽的DOM與對應的回調保存進Event。 //accumulateInto能夠理解成_.assign //記住這兩個屬性,很重要。 event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } }
listenerAtPhase裏面執行的是EventPluginHub的getListener函數
getListener: function (inst, registrationName) { //還記得以前保存回調的listenerBank吧? var bankForRegistrationName = listenerBank[registrationName]; if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) { return null; } //獲取inst的_rootNodeId var key = getDictionaryKey(inst); //獲取對應的回調 return bankForRegistrationName && bankForRegistrationName[key]; },
能夠發現,React在分裝原生nativeEvent時
runEventQueueInBatch
主要進行了兩步操做
function runEventQueueInBatch(events) { //將event事件加入processEventQueue序列 EventPluginHub.enqueueEvents(events); //前一步保存好的processEventQueue依次執行 //executeDispatchesAndRelease EventPluginHub.processEventQueue(false); } processEventQueue: function (simulated) { var processingEventQueue = eventQueue; eventQueue = null; if (simulated) { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); } else { //重點看這裏 //forEachAccumulated能夠當作forEach的封裝 //那麼這裏就是processingEventQueue保存的event依次執行executeDispatchesAndReleaseTopLevel(event) forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); } },
executeDispatchesAndReleaseTopLevel(event)
又是各類函數包裝,最後幹活的是
function executeDispatchesInOrder(event, simulated) { //對應的回調函數數組 var dispatchListeners = event._dispatchListeners; //有eventType屬性的ReactElement數組 var dispatchInstances = event._dispatchInstances; ...... if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { if (event.isPropagationStopped()) { break; } executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); } } else if (dispatchListeners) { executeDispatch(event, simulated, dispatchListeners, dispatchInstances); } event._dispatchListeners = null; event._dispatchInstances = null; }
OK,這裏總算出現了老熟人,在封裝nativeEvent時咱們保存在event裏的兩個屬性,dispatchListeners
與dispatchInstances
,在這裏起做用。
代碼很簡單,若是有處理這個事件的回調函數,就一次進行處理。細節咱們稍後討論,先看看這裏是怎麼處理的吧
function executeDispatch(event, simulated, listener, inst) { //type是事件類型 var type = event.type || 'unknown-event'; //這是觸發事件的真實DOM,也就是列子中的button event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { //看這裏看這裏 ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; }
終於來到最後了,代碼位於ReactErrorUtil中
(爲了幫助開發,React經過模擬真正的瀏覽器事件來得到更好的devtools集成。這段代碼在開發模式下運行)
//創造一個臨時DOM var fakeNode = document.createElement('react'); ReactErrorUtils.invokeGuardedCallback = function (name, func, a) { //綁定回調函數的上下文 var boundFunc = func.bind(null, a); //定義事件類型 var evtType = 'react-' + name; //綁定事件 fakeNode.addEventListener(evtType, boundFunc, false); //生成原生事件 var evt = document.createEvent('Event'); //將原生事件處理成咱們須要的類型 evt.initEvent(evtType, false, false); //發佈事件---這裏會執行回調 fakeNode.dispatchEvent(evt); //移出事件監聽 fakeNode.removeEventListener(evtType, boundFunc, false); };
不難發現,咱們經歷了從真實DOM到Virtual DOM的來回轉化。
怎麼避免二者影響
這個答案你們說了不少次,避免原生事件與React事件混用,或者經過target進行判斷。
在網上看過一個列子說得很好,一個Ul下面有1000個li標籤。想在想爲每一個li都綁定一個事件,怎麼操做?總不可能一個個綁定吧?其實這個和jquery綁定事件差很少。經過最外層綁定事件,當操做是點擊任何一個li天然會冒泡到最外面的Ul,又能夠經過最外面的target獲取到具體操做的DOM。一次綁定,收益一羣啊。