筆者將編寫"React原始碼解析"系列文章三到四篇,闡述React內部的機制。歡迎大家關注我的掘金賬號,以便能及時看到最新的文章更新推送。
在前面三篇文章中,我們闡述了react元件的構成與生命週期,setState的機制。這次我們來談談React的事件處理。
1.原生事件系統
我們通常監聽真實DOM。舉?來說,我們想監聽按鈕的點選事件,那麼我們在按鈕DOM上繫結事件和對應的回撥函式即可。 遺憾的是若頁面複雜且事件處理頻率高,那麼對網頁效能是個考驗。
2.React事件系統
react的事件處理再眼花繚亂終究還是要回歸原生的事件系統,但它做的封裝卻很優雅。我們直接上結論:
- React實現了SyntheticEvent層處理事件
什麼意思呢?詳細來說,React並不像原生事件一樣將事件和DOM一一對應,而是將所有的事件都繫結在網頁的document,通過統一的事件監聽器處理並分發,找到對應的回撥函式並執行。按照官方文件的說法,事件處理程式將傳遞SyntheticEvent的例項,那麼接下來我們一探SyntheticEvent的究竟。
3.SyntheticEvent
1.事件註冊
上文說到,既然React對事件統一進行處理,那麼肯定需要先註冊程式設計師寫的事件觸發函式吧?那麼這個過程是在哪裡執行的呢?因為我們是把事件"繫結"在"元件DOM"上,例如一個點選事件:
<Component onClick={this.handleClick}/>
複製程式碼
其實在這個元件掛載的時候,React就已經開始通過mountCompoent
內部的_updateDOMProperties
方法進行事件處理了。在這個方法中,執行的是enqueuePutListener
方法去註冊事件:
順藤摸瓜,listenTo
方法關鍵呼叫了以下兩個函式:
- trapBubbledEvent
- trapCapturedEvent
熟悉原生事件系統的讀者從英文翻譯就能知道,兩個函式是用來處理事件捕獲和事件冒泡的。具體處理邏輯不分析,我們直接看這兩個函式內部:
上述程式碼中的target
也就是document
,也看到了熟悉的document.addEventListener
和document.removeEventListener
。正是這樣統一的事件繫結減少了記憶體的開銷。
2.事件儲存
我們寫的事件回撥函式註冊完畢後需要儲存起來,以便觸發時進行回撥。儲存的入口是EventPluginHub.putListener
函式:
可見所有的回撥函式都以二維陣列的形式儲存在listenerBank
中,根據元件對應的key
來進行管理。
3.事件分發
事件註冊和事件儲存我們已經清楚了,現在我們看下當事件觸發時,React是如何進行事件分發和找到對應回撥函式並執行的。分發入口在ReactDOMEventListener.js
的handleTopLevelImpl
:
上述程式碼我們理清了流程:因為事件回撥函式執行後可能導致DOM結構的變化,那麼React先將當前的結構以陣列的形式儲存起來,依次遍歷執行。
上述函式的_handleTopLevel
最終對回撥函式進行處理,看下原始碼:
程式碼中出現了新角色:EventPluginHub.extractEvents
。查閱相關資料,得知extractEvents
方法是用於合成事件的,也就是根據事件型別的不同,合成不同的跨瀏覽器的SyntheticEvent
物件的例項,比如SyntheticClickEvent
。而EventPluginHub
顧名思義是React進行合成事件時所用的工具外掛:
可以看到對於不同的事件,React將使用不同的功能外掛,這些外掛都是通過依賴注入的方式進入內部使用的。React合成事件的過程非常繁瑣,但可以概括出extractEvents
函式內部主要是通過switch
函式區分事件型別並呼叫不同的外掛進行處理從而生成SyntheticEvent
例項。有興趣的同學可以自行了解。
4.事件處理
React處理事件的思想與處理setState
的思想類似,都是採用批處理的方法。在上面handleTopLevel
方法中我們看到最後執行了runEventQueueInBatch
方法:
//事件進入佇列
EventPluginHub.enqueueEvents(events);
//...
EventPluginHub.processEventQueue(false);
複製程式碼
看下processEventQueue
:
上述程式碼遍歷佇列中的事件,並進入executeDispatchesAndReleaseSimulated
:
event.constructor.release(event);
複製程式碼
這行程式碼將React的合成事件release掉,減少記憶體開銷。事件處理的核心入口在executeDispatchesInOrder
:
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
複製程式碼
重要的程式碼就這三行,dispatchListeners
是事件回撥函式,dispatchInstances
是對應的元件,將這些引數傳入executeDispatch
後:
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
複製程式碼
而invokeGuardedCallback
就相當簡單了:
function invokeGuardedCallback(name, func, a) {
func(a);
}
複製程式碼
上面的func(a)
其實就是listener(event)
,再往上追溯,就是dispatchListeners(dispatchInstances)
,這也就說明為什麼我們的React事件回撥函式可以拿到原生的事件了。
4.總結
React事件系統為了相容各種版本的瀏覽器而做了大量工作,我們不必鑽牛角尖去研究這些是如何實現的,與原生事件不同的點,只在於React對事件進行統一而不是分散的儲存與管理,捕獲事件後內部生成合成事件提高瀏覽器的相容度,執行回撥函式後再進行銷燬釋放記憶體,從而大大提高網頁的響應效能。
回顧:
《React原始碼解析(一):元件的實現與掛載》
《React原始碼解析(二):元件的型別與生命週期》
《React原始碼解析(三):詳解事務與佇列》
聯絡郵箱:ssssyoki@foxmail.com