【前端精華】React源碼分析系列


React源碼系列文章,請多支持:
React源碼分析1 — 組件和對象的創建(createClass,createElement)
React源碼分析2 — React組件插入DOM流程
React源碼分析3 — React生命周期詳解
React源碼分析4 — setState機制
React源碼分析5 -- 組件通信,refs,key,ReactDOM
React源碼分析6 — React合成事件系統

1 React合成事件特點

React自己實現了一套高效的事件注冊,存儲,分發和重用邏輯,在DOM事件體系基礎上做了很大改進,減少了內存消耗,簡化了事件邏輯,並最大化的解決了IE等瀏覽器的不兼容問題。與DOM事件體系相比,它有如下特點

  1. React組件上聲明的事件最終綁定到了document這個DOM節點上,而不是React組件對應的DOM節點。故只有document這個節點上面才綁定了DOM原生事件,其他節點沒有綁定事件。這樣簡化了DOM原生事件,減少了內存開銷
  2. React以隊列的方式,從觸發事件的組件向父組件回溯,調用它們在JSX中聲明的callback。也就是React自身實現了一套事件冒泡機制。我們沒辦法用event.stopPropagation()來停止事件傳播,應該使用event.preventDefault()
  3. React有一套自己的合成事件SyntheticEvent,不同類型的事件會構造不同的SyntheticEvent
  4. React使用對象池來管理合成事件對象的創建和銷毀,這樣減少了垃圾的生成和新對象內存的分配,大大提高了性能

那么這些特性是如何實現的呢,下面和大家一起一探究竟。

2 React事件系統

先看Facebook給出的React事件系統框圖

Markdown

瀏覽器事件(如用戶點擊了某個button)觸發后,DOM將event傳給ReactEventListener,它將事件分發到當前組件及以上的父組件。然后由ReactEventEmitter對每個組件進行事件的執行,先構造React合成事件,然后以queue的方式調用JSX中聲明的callback進行事件回調。

涉及到的主要類如下

ReactEventListener:負責事件注冊和事件分發。React將DOM事件全都注冊到document這個節點上,這個我們在事件注冊小節詳細講。事件分發主要調用dispatchEvent進行,從事件觸發組件開始,向父元素遍歷。我們在事件執行小節詳細講。

ReactEventEmitter:負責每個組件上事件的執行。

EventPluginHub:負責事件的存儲,合成事件以對象池的方式實現創建和銷毀,大大提高了性能。

SimpleEventPlugin等plugin:根據不同的事件類型,構造不同的合成事件。如focus對應的React合成事件為SyntheticFocusEvent

2 事件注冊

JSX中聲明一個React事件十分簡單,比如

render() { return ( <div onClick = { (event) => {console.log(JSON.stringify(event))} } /> ); } 

那么它是如何被注冊到React事件系統中的呢?

還是先得從組件創建和更新的入口方法mountComponent和updateComponent說起。在這兩個方法中,都會調用到_updateDOMProperties方法,對JSX中聲明的組件屬性進行處理。源碼如下

_updateDOMProperties: function (lastProps, nextProps, transaction) { ... // 前面代碼太長,省略一部分 else if (registrationNameModules.hasOwnProperty(propKey)) { // 如果是props這個對象直接聲明的屬性,而不是從原型鏈中繼承而來的,則處理它 // nextProp表示要創建或者更新的屬性,而lastProp則表示上一次的屬性 // 對於mountComponent,lastProp為null。updateComponent二者都不為null。unmountComponent則nextProp為null if (nextProp) { // mountComponent和updateComponent中,enqueuePutListener注冊事件 enqueuePutListener(this, propKey, nextProp, transaction); } else if (lastProp) { // unmountComponent中,刪除注冊的listener,防止內存泄漏 deleteListener(this, propKey); } } } 

下面我們來看enqueuePutListener,它負責注冊JSX中聲明的事件。源碼如下

// inst: React Component對象 // registrationName: React合成事件名,如onClick // listener: React事件回調方法,如onClick=callback中的callback // transaction: mountComponent或updateComponent所處的事務流中,React都是基於事務流的 function enqueuePutListener(inst, registrationName, listener, transaction) { if (transaction instanceof ReactServerRenderingTransaction) { return; } var containerInfo = inst._hostContainerInfo; var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE; // 找到document var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 注冊事件,將事件注冊到document上 listenTo(registrationName, doc); // 存儲事件,放入事務隊列中 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); } 

enqueuePutListener主要做兩件事,一方面將事件注冊到document這個原生DOM上(這就是為什么只有document這個節點有DOM事件的原因),另一方面采用事務隊列的方式調用putListener將注冊的事件存儲起來,以供事件觸發時回調。

注冊事件的入口是listenTo方法, 它解決了不同瀏覽器間捕獲和冒泡不兼容的問題。事件回調方法在bubble階段被觸發。如果我們想讓它在capture階段觸發,則需要在事件名上加上capture。比如onClick在bubble階段觸發,而onCaptureClick在capture階段觸發。listenTo代碼雖然比較長,但邏輯很簡單,調用trapCapturedEvent和trapBubbledEvent來注冊捕獲和冒泡事件。trapCapturedEvent大家可以自行分析,我們僅分析trapBubbledEvent,如下

  trapBubbledEvent: function (topLevelType, handlerBaseName, element) { if (!element) { return null; } return EventListener.listen( element, // 綁定到的DOM目標,也就是document handlerBaseName, // eventType ReactEventListener.dispatchEvent.bind(null, topLevelType)); // callback, document上的原生事件觸發后回調 }, listen: function listen(target, eventType, callback) { if (target.addEventListener) { // 將原生事件添加到target這個dom上,也就是document上。 // 這就是只有document這個DOM節點上有原生事件的原因 target.addEventListener(eventType, callback, false); return { // 刪除事件,這個由React自己回調,不需要調用者來銷毀。但僅僅對於React合成事件才行 remove: function remove() { target.removeEventListener(eventType, callback, false); } }; } else if (target.attachEvent) { // attach和detach的方式 target.attachEvent('on' + eventType, callback); return { remove: function remove() { target.detachEvent('on' + eventType, callback); } }; } }, 

在listen方法中,我們終於發現了熟悉的addEventListener這個原生事件注冊方法。只有document節點才會調用這個方法,故僅僅只有document節點上才有DOM事件。這大大簡化了DOM事件邏輯,也節約了內存。

流程圖如下

Markdown

3 事件存儲

事件存儲由EventPluginHub來負責,它的入口在我們上面講到的enqueuePutListener中的putListener方法,如下

  /** * EventPluginHub用來存儲React事件, 將listener存儲到`listenerBank[registrationName][key]` * * @param {object} inst: 事件源 * @param {string} listener的名字,比如onClick * @param {function} listener的callback */ // putListener: function (inst, registrationName, listener) { // 用來標識注冊了事件,比如onClick的React對象。key的格式為'.nodeId', 只用知道它可以標示哪個React對象就可以了 var key = getDictionaryKey(inst); var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); // 將listener事件回調方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId] // 所有React組件對象定義的所有React事件都會存儲在listenerBank中 bankForRegistrationName[key] = listener; //onSelect和onClick注冊了兩個事件回調插件, 用於walkAround某些瀏覽器兼容bug,不用care var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; if (PluginModule && PluginModule.didPutListener) { PluginModule.didPutListener(inst, registrationName, listener); } }, var getDictionaryKey = function (inst) { return '.' + inst._rootNodeID; }; 

由上可見,事件存儲在了listenerBank對象中,它按照事件名和React組件對象進行了二維划分,比如nodeId組件上注冊的onClick事件最后存儲在listenerBank.onclick[nodeId]中。

4 事件執行

4.1 事件分發

當事件觸發時,document上addEventListener注冊的callback會被回調。從前面事件注冊部分發現,此時回調函數為ReactEventListener.dispatchEvent,它是事件分發的入口方法。下面我們來詳細分析

// topLevelType:帶top的事件名,如topClick。不用糾結為什么帶一個top字段,知道它是事件名就OK了 // nativeEvent: 用戶觸發click等事件時,瀏覽器傳遞的原生事件 dispatchEvent: function (topLevelType, nativeEvent) { // disable了則直接不回調相關方法 if (!ReactEventListener._enabled) { return; } var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent); try { // 放入批處理隊列中,React事件流也是一個消息隊列的方式 ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } } 

可見我們仍然使用批處理的方式進行事件分發,handleTopLevelImpl才是事件分發的真正執行者,它是事件分發的核心,體現了React事件分發的特點,如下

// document進行事件分發,這樣具體的React組件才能得到響應。因為DOM事件是綁定到document上的 function handleTopLevelImpl(bookKeeping) { // 找到事件觸發的DOM和React Component var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget); // 執行事件回調前,先由當前組件向上遍歷它的所有父組件。得到ancestors這個數組。 // 因為事件回調中可能會改變Virtual DOM結構,所以要先遍歷好組件層級 var ancestor = targetInst; do { bookKeeping.ancestors.push(ancestor); ancestor = ancestor && findParent(ancestor); } while (ancestor); // 從當前組件向父組件遍歷,依次執行注冊的回調方法. 我們遍歷構造ancestors數組時,是從當前組件向父組件回溯的,故此處事件回調也是這個順序 // 這個順序就是冒泡的順序,並且我們發現不能通過stopPropagation來阻止'冒泡'。 for (var i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); } } 

從上面的事件分發中可見,React自身實現了一套冒泡機制。從觸發事件的對象開始,向父元素回溯,依次調用它們注冊的事件callback。

4.2 事件callback調用

事件處理由_handleTopLevel完成。它其實是調用ReactBrowserEventEmitter.handleTopLevel() ,如下

  // React事件調用的入口。DOM事件綁定在了document原生對象上,每次事件觸發,都會調用到handleTopLevel handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { // 采用對象池的方式構造出合成事件。不同的eventType的合成事件可能不同 var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); // 批處理隊列中的events runEventQueueInBatch(events); } 

handleTopLevel方法是事件callback調用的核心。它主要做兩件事情,一方面利用瀏覽器回傳的原生事件構造出React合成事件,另一方面采用隊列的方式處理events。先看如何構造合成事件。

4.2.1 構造合成事件

  // 構造合成事件 extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { var events; // EventPluginHub可以存儲React合成事件的callback,也存儲了一些plugin,這些plugin在EventPluginHub初始化時就注冊就來了 var plugins = EventPluginRegistry.plugins; for (var i = 0; i < plugins.length; i++) { var possiblePlugin = plugins[i]; if (possiblePlugin) { // 根據eventType構造不同的合成事件SyntheticEvent var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); if (extractedEvents) { // 將構造好的合成事件extractedEvents添加到events數組中,這樣就保存了所有plugin構造的合成事件 events = accumulateInto(events, extractedEvents); } } } return events; }, 

EventPluginRegistry.plugins默認包含五種plugin,他們是在EventPluginHub初始化階段注入進去的,且看代碼

  // 將eventPlugin注冊到EventPluginHub中 ReactInjection.EventPluginHub.injectEventPluginsByName({  SimpleEventPlugin: SimpleEventPlugin,  EnterLeaveEventPlugin: EnterLeaveEventPlugin,  ChangeEventPlugin: ChangeEventPlugin,  SelectEventPlugin: SelectEventPlugin,  BeforeInputEventPlugin: BeforeInputEventPlugin }); 

不同的plugin針對不同的事件有特殊的處理,此處我們不展開講了,下面僅分析SimpleEventPlugin中方法即可。

我們先看SimpleEventPlugin如何構造它所對應的React合成事件。

  // 根據不同事件類型,比如click,focus構造不同的合成事件SyntheticEvent, 如SyntheticKeyboardEvent SyntheticFocusEvent extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; if (!dispatchConfig) { return null; } var EventConstructor; // 根據事件類型,采用不同的SyntheticEvent來構造不同的合成事件 switch (topLevelType) { ... // 省略一些事件,我們僅以blur和focus為例 case 'topBlur': case 'topFocus': EventConstructor = SyntheticFocusEvent; break; ... // 省略一些事件 } // 從event對象池中取出合成事件對象,利用對象池思想,可以大大降低對象創建和銷毀的時間,提高性能。這是React事件系統的一大亮點 var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget); EventPropagators.accumulateTwoPhaseDispatches(event); return event; }, 

這里我們看到了event對象池這個重大特性,采用合成事件對象池的方式,可以大大降低銷毀和創建合成事件帶來的性能開銷。

對象創建好之后,我們還會將它添加到events這個隊列中,因為事件回調的時候會用到這個隊列。添加到events中使用的是accumulateInto方法。它思路比較簡單,將新創建的合成對象的引用添加到之前創建好的events隊列中即可,源碼如下

function accumulateInto(current, next) { if (current == null) { return next; } // 將next添加到current中,返回一個包含他們兩個的新數組 // 如果next是數組,current不是數組,采用push方法,否則采用concat方法 // 如果next不是數組,則返回一個current和next構成的新數組 if (Array.isArray(current)) { if (Array.isArray(next)) { current.push.apply(current, next); return current; } current.push(next); return current; } if (Array.isArray(next)) { return [current].concat(next); } return [current, next]; } 

4.2.2 批處理合成事件

我們上面分析過了,React以隊列的形式處理合成事件。方法入口為runEventQueueInBatch,如下

  function runEventQueueInBatch(events) { // 先將events事件放入隊列中 EventPluginHub.enqueueEvents(events); // 再處理隊列中的事件,包括之前未處理完的。先入先處理原則 EventPluginHub.processEventQueue(false); } /** * syntheticEvent放入隊列中,等到processEventQueue再獲得執行 */ enqueueEvents: function (events) { if (events) { eventQueue = accumulateInto(eventQueue, events); } }, /** * 分發執行隊列中的React合成事件。React事件是采用消息隊列方式批處理的 * * simulated:為true表示React測試代碼,我們一般都是false */ processEventQueue: function (simulated) { // 先將eventQueue重置為空 var processingEventQueue = eventQueue; eventQueue = null; if (simulated) { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); } else { // 遍歷處理隊列中的事件, // 如果只有一個元素,則直接executeDispatchesAndReleaseTopLevel(processingEventQueue) // 否則遍歷隊列中事件,調用executeDispatchesAndReleaseTopLevel處理每個元素 forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); } // This would be a good time to rethrow if any of the event handlers threw. ReactErrorUtils.rethrowCaughtError(); }, 

合成事件處理也分為兩步,先將我們要處理的events隊列放入eventQueue中,因為之前可能就存在還沒處理完的合成事件。然后再執行eventQueue中的事件。可見,如果之前有事件未處理完,這里就又有得到執行的機會了。

事件執行的入口方法為executeDispatchesAndReleaseTopLevel,如下

var executeDispatchesAndReleaseTopLevel = function (e) { return executeDispatchesAndRelease(e, false); }; var executeDispatchesAndRelease = function (event, simulated) { if (event) { // 進行事件分發, EventPluginUtils.executeDispatchesInOrder(event, simulated); if (!event.isPersistent()) { // 處理完,則release掉event對象,采用對象池方式,減少GC // React幫我們處理了合成事件的回收機制,不需要我們關心。但要注意,如果使用了DOM原生事件,則要自己回收 event.constructor.release(event); } } }; // 事件處理的核心 function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; if (Array.isArray(dispatchListeners)) { // 如果有多個listener,則遍歷執行數組中event for (var i = 0; i < dispatchListeners.length; i++) { // 如果isPropagationStopped設成true了,則停止事件傳播,退出循環。 if (event.isPropagationStopped()) { break; } // 執行event的分發,從當前觸發事件元素向父元素遍歷 // event為瀏覽器上傳的原生事件 // dispatchListeners[i]為JSX中聲明的事件callback // dispatchInstances[i]為對應的React Component executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); } } else if (dispatchListeners) { // 如果只有一個listener,則直接執行事件分發 executeDispatch(event, simulated, dispatchListeners, dispatchInstances); } // 處理完event,重置變量。因為使用的對象池,故必須重置,這樣才能被別人復用 event._dispatchListeners = null; event._dispatchInstances = null; } 

executeDispatchesInOrder會先得到event對應的listeners隊列,然后從當前元素向父元素遍歷執行注冊的callback。且看executeDispatch

function executeDispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { // test代碼使用,支持try-catch,其他就沒啥區別了 ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { // 事件分發,listener為callback,event為參數,類似listener(event)這個方法調用 // 這樣就回調到了我們在JSX中注冊的callback。比如onClick={(event) => {console.log(1)}} // 這樣應該就明白了callback怎么被調用的,以及event參數怎么傳入callback里面的了 ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; } // 采用func(a)的方式進行調用, // 故ReactErrorUtils.invokeGuardedCallback(type, listener, event)最終調用的是listener(event) // event對象為瀏覽器傳遞的DOM原生事件對象,這也就解釋了為什么React合成事件回調中能拿到原生event的原因 function invokeGuardedCallback(name, func, a) { try { func(a); } catch (x) { if (caughtError === null) { caughtError = x; } }
}
全文點擊:http://click.aliyun.com/m/14337/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM