上篇文章咱們瞭解了React合成事件跟原生綁定事件是有區別的,本篇文章從源碼來深挖一下React的事件機制。html
TL;DR :node
仍是使用上次的栗子:react
class ExampleApplication extends React.Component { componentDidMount() { document.addEventListener('click', () => { alert('document click'); }) } outClick(e) { console.log(e.currentTarget); alert('outClick'); } onClick(e) { console.log(e.currentTarget); alert('onClick'); e.stopPropagation(); } render() { return <div onClick={this.outClick}> <button onClick={this.onClick}> 測試click事件 </button> </div> } }
分析源碼以前,有些工做和知識要提早準備,普及一下:git
// 做用:若是隻是單個next,則直接返回,若是有數組,返回合成的數組,裏面有個 //current.push.apply(current, next)能夠學習一下,我查了一下[資料][3]https://jsperf.com/array-prototype-push-apply-vs-concat/2,這樣組合數組比concat效率更高 // 栗子:input accumulateInto([],[]) function accumulateInto(current, next) { if (current == null) { return next; } // Both are not empty. Warning: Never call x.concat(y) when you are not // certain that x is an Array (x could be a string with concat method). if (Array.isArray(current)) { if (Array.isArray(next)) { current.push.apply(current, next); return current; } current.push(next); return current; } if (Array.isArray(next)) { // A bit too dangerous to mutate `next`. return [current].concat(next); } return [current, next]; } // 這個其實就是用來執行函數的,當arr時數組的時候,arr裏的每個項都做爲回調函數cb的參數執行; // 若是不是數組,直接執行回調函數cb,參數爲arr // 例如: // arr爲數組:forEachAccumulated([1,2,3], (item) => {console.log(item), this}) // 此時會打印出 1,2,3 // arr不爲數組,forEachAccumulated(1, (item) => {console.log(item), this}) // 此時會打印出 1 function forEachAccumulated(arr, cb, scope) { if (Array.isArray(arr)) { arr.forEach(cb, scope); } else if (arr) { cb.call(scope, arr); } }
React事件機制分爲兩塊:github
咱們一步步來看:web
整個過程從ReactDomComponent
開始,重點在enqueuePutListener
,這個函數作了三件事情,詳細請參考下面源碼:segmentfault
function enqueuePutListener () { // 省略部分代碼 ... // 一、*重要:在這裏取出button所在的document* var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 二、在document上註冊事件,同一個事件類型只會被註冊一次 listenTo(registrationName, doc); // 三、mountReady以後將回調函數存在ListernBank中 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); }
接下來看看第二步:在document上註冊事件 的過程,流程圖以下:
數組
接着咱們抽出每一個文件的重點函數出來分析:瀏覽器
listenTo: function (registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; // 檢測document上是否已經監聽onClick事件,因此前面說同一類型事件只會綁定一次 var isListening = getListeningForDocument(mountAt); // 得到dependency,將onClick 轉成topClick,這只是一種處理方式不用糾結 var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName]; // 中間是對各類事件類型給document綁定捕獲事件或者冒泡事件,大部分都是冒泡, ... // 這裏咱們的topClick,綁定的是冒泡事件 else if (topEventMapping.hasOwnProperty(dependency)) { // trapBubbledEvent會在下面分析 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt); } // 最後把topClick標記爲已註冊過,防止重複註冊 isListening[dependency] = true; }
因爲onclick綁定的是冒泡事件,因此咱們來看看trapBubbledEvent
app
// 輸入: topClick, click, doc trapBubbledEvent: function (topLevelType, handlerBaseName, element) { if (!element) { return null; } // EventListener 要作的事情就是把事件綁定到document上,注意這裏不管是註冊冒泡仍是捕獲事件,最終的回調函數都是dispatchEvent return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType)); }, // EventListener.js // 輸入doc, click, dispatchEvent // 這個函數其實就是咱們熟悉的兼容瀏IE瀏覽器事件綁定的方法 listen: function listen(target, eventType, callback) { if (target.addEventListener) { target.addEventListener(eventType, callback, false); return { remove: function remove() { target.removeEventListener(eventType, callback, false); } }; } else if (target.attachEvent) { target.attachEvent('on' + eventType, callback); return { remove: function remove() { target.detachEvent('on' + eventType, callback); } }; } },
注意這裏不管是註冊冒泡仍是捕獲事件,最終的回調函數都是dispatchEvent,因此咱們來看看dispatchEvent
怎麼處理事件分發。
看到這裏你們會奇怪,全部的事件的回調函數都是dispatchEvent
來處理,那事件onClick
原來的回調函數存到哪裏去了呢?
再回來看事件註冊的第三步:mountReady以後將回調函數存在ListernBank中
function enqueuePutListener () { // 省略部分代碼 ... // 一、*重要:在這裏取出button所在的document* var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 二、在document上註冊事件,同一個事件類型只會被註冊一次 listenTo(registrationName, doc); // 三、mountReady以後將回調函數存在ListernBank中 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); }
在document
上註冊完全部的事件以後,還須要把listener
放到listenerBank
中以listenerBank[registrationName][key]
這樣的形式存起來,而後在dispatchEvent
裏面使用。
將listener放到listenerBank中儲存的過程以下:
// 在putListener裏存入listener function putListener() { var listenerToPut = this; // 先put的是外層的listener - outClick,因此這裏的inst是外層div // registrationName是onclick,listener是outClick EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); }
/** * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent. * * @param {object} inst The instance, which is the source of events. * @param {string} registrationName Name of listener (e.g. `onClick`). * @param {function} listener The callback to store. */ putListener: function (inst, registrationName, listener) { var key = getDictionaryKey(inst); // 先根據inst獲得惟一的key var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); // 能夠看到最終listener 在 listenerBank裏,最終以listenerBank[registrationName][key] 存在 bankForRegistrationName[key] = listener; var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; if (PluginModule && PluginModule.didPutListener) { // 這裏的didPutListener只是爲了兼容手機safari對non-interactive元素 // 雙擊響應不正確,詳情能夠參考這篇[文章][7] //https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html PluginModule.didPutListener(inst, registrationName, listener); } },
以上就是事件註冊的過程,接下來在看dispatchEvent如何處理事件分發。
在介紹事件分發以前,有必要先介紹一下生成合成事件的過程,連接是https://segmentfault.com/a/11...
瞭解合成事件生成的過程以後,咱們須要get一個點:合成事件收集了一波同類型(例如click
)的回調函數存在了合成事件event._dispatchListeners
這個數組裏,而後將它們事件對應的虛擬dom節點放到_dispatchInstances
就本例來講,_dispatchListeners= [onClick, outClick]
,以後在一塊兒執行。
接下來看看事件分發的過程:
dispatchEvent: function (topLevelType, nativeEvent) { if (!ReactEventListener._enabled) { return; } // 這裏獲得TopLevelCallbackBookKeeping的實例對象,本例中第一次觸發dispatchEvent時 // bookKeeping = {ancestors: [],nativeEvent,‘topClick’} var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent); try { // Event queue being processed in the same cycle allows // `preventDefault`. // 接着執行handleTopLevelImpl(bookKeeping) ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } } function handleTopLevelImpl(bookKeeping) { var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); // 獲取當前事件的虛擬dom元素 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]; // 這裏的_handleTopLevel 對應的就是ReactEventEmitterMixin.js裏的handleTopLevel ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); } } // 這裏的findParent曾經給我帶來誤導,我覺得去找當前元素全部的父節點,但其實不是的, // 咱們知道通常狀況下,咱們的組件最後會被包裹在<div id='root'></div>的標籤裏 // 通常是沒有組件再去嵌套它的,因此一般返回null /** * Find the deepest React component completely containing the root of the * passed-in instance (for use when entire React trees are nested within each * other). If React trees are not nested, returns null. */ function findParent(inst) { while (inst._hostParent) { inst = inst._hostParent; } var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst); var container = rootNode.parentNode; return ReactDOMComponentTree.getClosestInstanceFromNode(container); }
上面這段代碼的重點就是_handleTopLevel
,它能夠獲取合成事件,而且去執行它。
下面看看具體是如何執行:
function runEventQueueInBatch(events) { // 一、先將事件放進隊列裏 EventPluginHub.enqueueEvents(events); // 二、執行它 EventPluginHub.processEventQueue(false); } var ReactEventEmitterMixin = { /** * Streams a fired top-level event to `EventPluginHub` where plugins have the * opportunity to create `ReactEvent`s to be dispatched. */ handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { // 用EventPluginHub生成合成事件 var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); // 執行合成事件 runEventQueueInBatch(events); } };
執行的過程分紅兩步:
執行的細節以下:
var executeDispatchesAndReleaseTopLevel = function (e) { return executeDispatchesAndRelease(e, false); }; var executeDispatchesAndRelease = function (event, simulated) { if (event) { // 在這裏dispatch事件 EventPluginUtils.executeDispatchesInOrder(event, simulated); // 釋放事件 if (!event.isPersistent()) { event.constructor.release(event); } } }; enqueueEvents: function (events) { if (events) { eventQueue = accumulateInto(eventQueue, events); } }, /** * Dispatches all synthetic events on the event queue. * * @internal */ processEventQueue: function (simulated) { // Set `eventQueue` to null before processing it so that we can tell if more // events get enqueued while processing. var processingEventQueue = eventQueue; eventQueue = null; if (simulated) { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); } else { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); } // This would be a good time to rethrow if any of the event fexers threw. ReactErrorUtils.rethrowCaughtError(); },
上段代碼裏,咱們最終會走到
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
forEachAccumulated
這個函數咱們以前講過,就是對數組processingEventQueue的每個合成事件都使用executeDispatchesAndReleaseTopLevel
來dispatch 事件。
因此各位同窗們,注意到這裏咱們已經走到最核心的部分,dispatch 合成事件了,下面看看dispatch的詳細過程:
/** * Standard/simple iteration through an event's collected dispatches. */ function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { // 由這裏能夠看出,合成事件的stopPropagation只能阻止react合成事件的冒泡, // 由於event._dispatchListeners 只記錄了由jsx綁定的綁定的事件,對於原生綁定的是沒有記錄的 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; }
由上面的函數可知,dispatch 合成事件分爲兩個步驟:
當回調函數裏使用了stopPropagation會使得數組後面的回調函數不能執行,這樣就作到了阻止事件冒泡
目前仍是還有看到執行事件的代碼,在接着看:
function executeDispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; // 注意這裏將事件對應的dom元素綁定到了currentTarget上 event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { // 通常都是非模擬的狀況,執行invokeGuardedCallback ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; }
上面這個函數最重要的功能就是將事件對應的dom元素綁定到了currentTarget上,
這樣咱們經過e.currentTarget就能夠找到綁定事件的原生dom元素。
下面就是整個執行過程的尾聲了:
var fakeNode = document.createElement('react'); ReactErrorUtils.invokeGuardedCallback = function (name, func, a) { var boundFunc = function () { func(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); };
由invokeGuardedCallback
可知,最後react調用了faked元素的dispatchEvent方法來觸發事件,而且觸發完畢以後當即移除監聽事件。
總的來講,整個click事件被分發的過程就是:
一、用EventPluginHub生成合成事件,這裏注意同一事件類型只會生成一個合成事件,裏面的_dispatchListeners裏儲存了同一事件類型的全部回調函數
二、按順序去執行它
就辣麼簡單!
本文比較長,有不理解的歡迎提問~ 或者有理解錯誤的也請你們指正。
最後附上整個流程圖文件:
s://segmentfault.com/a/1190000013343819