React原始碼解析(四):事件系統

ssssyoki發表於2017-12-21

筆者將編寫"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方法去註冊事件:

React原始碼解析(四):事件系統

順藤摸瓜,listenTo方法關鍵呼叫了以下兩個函式:

  • trapBubbledEvent
  • trapCapturedEvent

熟悉原生事件系統的讀者從英文翻譯就能知道,兩個函式是用來處理事件捕獲和事件冒泡的。具體處理邏輯不分析,我們直接看這兩個函式內部:

React原始碼解析(四):事件系統

上述程式碼中的target也就是document,也看到了熟悉的document.addEventListenerdocument.removeEventListener。正是這樣統一的事件繫結減少了記憶體的開銷。

2.事件儲存

我們寫的事件回撥函式註冊完畢後需要儲存起來,以便觸發時進行回撥。儲存的入口是EventPluginHub.putListener函式:

React原始碼解析(四):事件系統

可見所有的回撥函式都以二維陣列的形式儲存在listenerBank中,根據元件對應的key來進行管理。

3.事件分發

事件註冊和事件儲存我們已經清楚了,現在我們看下當事件觸發時,React是如何進行事件分發和找到對應回撥函式並執行的。分發入口在ReactDOMEventListener.jshandleTopLevelImpl:

React原始碼解析(四):事件系統

上述程式碼我們理清了流程:因為事件回撥函式執行後可能導致DOM結構的變化,那麼React先將當前的結構以陣列的形式儲存起來,依次遍歷執行。 上述函式的_handleTopLevel最終對回撥函式進行處理,看下原始碼:

React原始碼解析(四):事件系統

程式碼中出現了新角色:EventPluginHub.extractEvents。查閱相關資料,得知extractEvents方法是用於合成事件的,也就是根據事件型別的不同,合成不同的跨瀏覽器的SyntheticEvent物件的例項,比如SyntheticClickEvent。而EventPluginHub顧名思義是React進行合成事件時所用的工具外掛:

React原始碼解析(四):事件系統

可以看到對於不同的事件,React將使用不同的功能外掛,這些外掛都是通過依賴注入的方式進入內部使用的。React合成事件的過程非常繁瑣,但可以概括出extractEvents函式內部主要是通過switch函式區分事件型別並呼叫不同的外掛進行處理從而生成SyntheticEvent例項。有興趣的同學可以自行了解。

4.事件處理

React處理事件的思想與處理setState的思想類似,都是採用批處理的方法。在上面handleTopLevel方法中我們看到最後執行了runEventQueueInBatch方法:

    //事件進入佇列
    EventPluginHub.enqueueEvents(events);
    //...
    EventPluginHub.processEventQueue(false);
複製程式碼

看下processEventQueue

React原始碼解析(四):事件系統

上述程式碼遍歷佇列中的事件,並進入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

相關文章