react原始碼-事件監聽
本文以 React v16.5.2 為基礎進行原始碼分析
基本流程
在 react原始碼的 react-dom/src/events/ReactBrowserEventEmitter.js檔案的開頭,有這麼一大段註釋:

事件委託是很常用的一種瀏覽器事件優化策略,於是 React就接管了這件事情,並且還貼心地消除了瀏覽器間的差異,賦予開發者跨瀏覽器的開發體驗,主要是使用 EventPluginHub這個東西來負責排程事件的儲存,合成事件並以物件池的方式實現建立和銷燬,至於下面的結構圖形,則是對事件機制的一個圖形化描述
- React事件使用了事件委託的機制,一般事件委託的作用都是為了減少頁面的註冊事件數量,減少記憶體開銷,優化瀏覽器效能,React這麼做也是有這麼一個目的,除此之外,也是為了能夠更好的管理事件,實際上,React中所有的事件最後都是被委託到了 document這個頂級DOM上
- 既然所有的事件都被委託到了 document上,那麼肯定有一套管理機制,所有的事件都是以一種先進先出的佇列方式進行觸發與回撥
- 既然都已經接管事件了,那麼不對事件做些額外的事情未免有些浪費,於是 React中就存在了自己的 合成事件(SyntheticEvent),合成事件由對應的 EventPlugin負責合成,不同型別的事件由不同的 plugin合成,例如 SimpleEvent Plugin、TapEvent Plugin等
- 為了進一步提升事件的效能,使用了 EventPluginHub這個東西來負責合成事件物件的建立和銷燬
#開始
<button onClick={this.autoFocus}>點選聚焦</button> 複製程式碼
這是我們在React中繫結事件的常規寫法。經由JSX解析,button會被當做元件掛載。而onClick這時候也只是一個普通的props。 ReactDOMComponent在進行元件載入(mountComponent)、更新(updateComponent)的時候,需要對props進行處理(_updateDOMProperties):
事件註冊
ReactDOMComponent.Mixin = { mountComponent:function(){}, _createOpenTagMarkupAndPutListeners:function(){}, ...., // 方法中有指向上次屬性值得lastProp, // nextProp是當前屬性值,這裡nextProp是我們繫結給元件的onclick事件處理函式。 //nextProp 不為空呼叫enqueuePutListener繫結事件,為空則登出事件繫結。 _updateDOMProperties:function(lastProps, nextProps, transaction){ for (propKey in lastProps) {}//省略。。。 for (propKey in nextProps) { // 判斷是否為事件屬性 if (registrationNameModules.hasOwnProperty(propKey)){ enqueuePutListener(this, propKey, nextProp, transaction); } } } } //這裡進行事件繫結 首先判斷了 rootContainerElement是不是一個 document或者 Fragment(文件片段節點) enqueuePutListener 這個方法只在瀏覽器環境下執行,傳給listenTo引數分別是事件名稱'onclick'和代理事件的綁 定dom。如果是fragement 就是根節點(在reactDom.render指定的),不是的話就是document。listenTo 用於繫結事件到 document ,下面交由事務處理的是回撥函式的儲存,便於呼叫。 ReactBrowserEventEmitter 檔案中的 listenTo 看做事件處理的源頭。 這裡獲取了當前元件(其實這時候就是button)所在的document function enqueuePutListener(inst, registrationName, listener, transaction) { ... var containerInfo = inst._hostContainerInfo; var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE; var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; listenTo(registrationName, doc); ... } 複製程式碼
繫結的重點是這裡的listenTo方法。看原始碼(ReactBrowerEventEmitter)
//registrationName:需要繫結的事件 //當前component所屬的document,即事件需要繫結的位置 listenTo: function (registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; //獲取當前document上已經繫結的事件 var isListening = getListeningForDocument(mountAt); // 獲取 registrationName(註冊事件名稱)的topLevelEvent(頂級事件型別) var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName]; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { if (dependency === 'topWheel') { ... } else if (dependency === 'topScroll') { ... } else if (dependency === 'topFocus' || dependency === 'topBlur') { ... } else if (topEventMapping.hasOwnProperty(dependency)) { // 獲取 topLevelEvent 對應的瀏覽器原生事件 //冒泡處理 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt); } isListening[dependency] = true; } } }, 複製程式碼
對於同一個事件,例如click有兩個事件 onClick(在冒泡階段觸發) onClickCapture(在捕獲階段觸發)兩個事件名,這個冒泡和捕獲都是react事件模擬出來的。繫結到 document上面的事件基本上都是在冒泡階段(對 whell, focus, scroll 有額外處理),如下圖 click 事件繫結執行的如下。
最後處理(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); } }; } 複製程式碼
所有事件繫結在document上
所以事件觸發的都是ReactEventListener的dispatch方法
回撥儲存
看到這邊你可能疑惑,所有回撥都執行的ReactEventListener的dispatch方法,那我寫的回調幹嘛去了。別急,接著看:
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來進行管理
// 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中。
是不是有點暈,放上流程圖,仔細回憶一下

事件觸發
註冊事件時我們說過,所有的事件都是繫結在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]; }, 複製程式碼
事件分發
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); }; 複製程式碼