本文做者: 江水
React 爲咱們提供了一套虛擬的事件系統,這套虛擬事件系統是如何工做的,筆者對源碼作了一次梳理,整理了下面的文檔供你們參考。html
在 React事件介紹 中介紹了合成事件對象以及爲何提供合成事件對象,主要緣由是由於 React 想實現一個全瀏覽器的框架, 爲了實現這種目標就須要提供全瀏覽器一致性的事件系統,以此抹平不一樣瀏覽器的差別。前端
合成事件對象頗有意思,一開始聽名字會以爲很奇怪,看到英文名更奇怪 SyntheticEvent
, 實際上合成事件的意思就是使用原生事件合成一個 React 事件, 例如使用原生click
事件合成了onClick
事件,使用原生mouseout
事件合成了onMouseLeave
事件,原生事件和合成事件類型大部分都是一一對應,只有涉及到兼容性問題時咱們才須要使用不對應的事件合成。合成事件並非 React 的獨創,在 iOS 上遇到的 300ms 問題而引入的 fastclick 就使用了 touch 事件合成了 click 事件,也算一種合成事件的應用。react
瞭解了 React 事件是合成事件以後咱們看待事件的角度就會有所不一樣, 例如咱們常常在代碼中寫的這種代碼git
<button onClick={handleClick}> Activate Lasers </button>
咱們已經知道這個onClick
只是一個合成事件而不是原生事件, 那這段時間究竟發生了什麼? 原生事件和合成事件是如何對應起來的?
上面的代碼看起來很簡潔,實際上 React 事件系統工做機制比起上面要複雜的多,髒活累活全都在底層處理了, 簡直框架勞模。其工做原理大致上分爲兩個階段github
下面就一塊兒來看下這兩個階段到底是如何工做的, 這裏主要從源碼層分析,並以 16.13 源碼中內容爲基準。數組
React 既然提供了合成事件,就須要知道合成事件與原生事件是如何對應起來的,這個對應關係存放在 React 事件插件中EventPlugin
, 事件插件能夠認爲是 React 將不一樣的合成事件處理函數封裝成了一個模塊,每一個模塊只處理本身對應的合成事件,這樣不一樣類型的事件種類就能夠在代碼上解耦,例如針對onChange
事件有一個單獨的LegacyChangeEventPlugin
插件來處理,針對onMouseEnter
, onMouseLeave
使用 LegacyEnterLeaveEventPlugin
插件來處理。瀏覽器
爲了知道合成事件與原生事件的對應關係,React 在一開始就將事件插件所有加載進來, 這部分邏輯在 ReactDOMClientInjection 代碼以下架構
injectEventPluginsByName({ SimpleEventPlugin: LegacySimpleEventPlugin, EnterLeaveEventPlugin: LegacyEnterLeaveEventPlugin, ChangeEventPlugin: LegacyChangeEventPlugin, SelectEventPlugin: LegacySelectEventPlugin, BeforeInputEventPlugin: LegacyBeforeInputEventPlugin });
註冊完上述插件後, EventPluginRegistry (老版本代碼裏這個模塊喚做EventPluginHub
)這個模塊裏就初始化好了一些全局對象,有幾個對象比較重要,能夠單獨說一下。app
第一個對象是 registrationNameModule, 它包含了 React 事件到它對應的 plugin 的映射, 大體長下面這樣,它包含了 React 所支持的全部事件類型,這個對象最大的做用是判斷一個組件的 prop 是不是事件類型,這在處理原生組件的 props 時候將會用到,若是一個 prop 在這個對象中才會被當作事件處理。框架
{ onBlur: SimpleEventPlugin, onClick: SimpleEventPlugin, onClickCapture: SimpleEventPlugin, onChange: ChangeEventPlugin, onChangeCapture: ChangeEventPlugin, onMouseEnter: EnterLeaveEventPlugin, onMouseLeave: EnterLeaveEventPlugin, ... }
第二個對象是 registrationNameDependencies, 這個對象長下面幾個樣子
{ onBlur: ['blur'], onClick: ['click'], onClickCapture: ['click'], onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'], onMouseEnter: ['mouseout', 'mouseover'], onMouseLeave: ['mouseout', 'mouseover'], ... }
這個對象便是一開始咱們說到的合成事件到原生事件的映射,對於onClick
和 onClickCapture
事件, 只依賴原生click
事件。可是對於 onMouseLeave
它倒是依賴了兩個mouseout
, mouseover
, 這說明這個事件是 React 使用 mouseout
和 mouseover
模擬合成的。正是由於這種行爲,使得 React 可以合成一些哪怕瀏覽器不支持的事件供咱們代碼裏使用。
第三個對象是 plugins, 這個對象就是上面註冊的全部插件列表。
plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
看完上面這些信息後咱們再反過頭來看下一個普通的EventPlugin
長什麼樣子。一個 plugin 就是一個對象, 這個對象包含了下面兩個屬性
// event plugin { eventTypes, // 一個數組,包含了全部合成事件相關的信息,包括其對應的原生事件關係 extractEvents: // 一個函數,當原生事件觸發時執行這個函數 }
瞭解上面這這些信息對咱們分析 React 事件工做原理將會頗有幫助,下面開始進入事件綁定階段。
document
上,回調爲React提供的dispatchEvent函數。
上面的階段說明:
document
上。dispatchEvent
函數。onClick
, 最終反應在 DOM 事件上只會有一個listener
。listener
綁在原生事件上,也沒有去維護一個相似eventlistenermap
的東西存放咱們的listener
。由 3,4 條規則能夠得出,咱們業務邏輯的listener
和實際 DOM 事件壓根就不要緊,React 只是會確保這個原生事件可以被它本身捕捉到,後續由 React 來派發咱們的事件回調,當咱們頁面發生較大的切換時候,React 能夠什麼都不作,從而免去了去操做removeEventListener
或者同步eventlistenermap
的操做,因此其執行效率將會大大提升,至關於全局給咱們作了一次事件委託,即使是渲染大列表,也不用開發者關心事件綁定問題。
咱們知道因爲全部類型種類的事件都是綁定爲React的 dispatchEvent
函數,因此就能在全局處理一些通用行爲,下面就是整個行爲過程。
export function dispatchEventForLegacyPluginEventSystem( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, ): void { const bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, eventSystemFlags ); try { // Event queue being processed in the same cycle allows // `preventDefault`. batchedEventUpdates(handleTopLevel, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); } }
bookKeeping
爲事件執行時組件的層級關係存儲,也就是若是在事件執行過程當中發生組件結構變動,並不會影響事件的觸發流程。
整個觸發事件流程以下:
dispatchEvent
函數。dispatchEvent
執行 batchedEventUpdates(handleTopLevel)
, batchedEventUpdates 會打開批量渲染開關並調用 handleTopLevel
。對於大部分事件而言其處理邏輯以下,也即 LegacySimpleEventPlugin
插件作的工做
SyntheticMouseEvent
) 。
div
, a
這種原生組件)。
onClickCapture
的實例。
onClick
的實例。
這幾個階段說明了下面的現象:
event.persist()
告訴 React 這個對象須要持久化。( React17 中被廢棄)onClick
事件, 在執行這些onClick
以前 React 會打開批量渲染開關,這個開關會將全部的setState
變成異步函數。onClick
。document
上.這點很好理解,React的事件實際上都是在document
上觸發的。
因此下面就是錯誤用法
function onClick(event) { setTimeout(() => { console.log(event.target.value); }, 100); }
setState
都會變成異步。function onClick(event) { setState({a: 1}); // 1 setState({a: 2}); // 2 setTimeout(() => { setState({a: 3}); // 3 setState({a: 4}); // 4 }, 0); }
此時 1, 2 在事件內因此是異步的,兩者只會觸發一次 render 操做,3, 4 是同步的,3,4 分別都會觸發一次 render。
onClick
/onClickCapture
, 實際上都發生在原生事件的冒泡階段。document.addEventListener('click', console.log.bind(null, 'native')); function onClickCapture() { console.log('capture'); } <div onClickCapture={onClickCapture}/>
這裏咱們雖然使用了onClickCapture
, 但實際上對原生事件而言依然是冒泡,因此 React 16 中實際上就不支持綁定捕獲事件。
ReactDOM.render
會存在衝突。若是咱們渲染一個子樹使用另外一個版本的 React 實例建立, 那麼即便在子樹中調用了 e.stopPropagatio
事件依然會傳播。因此多版本的 React 在事件上存在衝突。
最後咱們就能夠輕鬆理解 React 事件系統的架構圖了
React 17 目前已經發布了, 官方稱之爲沒有新特性的更新, 對於使用者而言沒有提供相似 Hooks 這樣爆炸的特性,也沒有 Fiber 這樣的重大重構,而是積攢了大量 Bugfix,修復了以前存在的諸多缺陷。其中變化最大的就數對事件系統的改造了。
下面是筆者列舉的一些事件相關的特性更新
將頂層事件綁定在 container
上而不是 document
上可以解決咱們遇到的多版本共存問題,對微前端方案是個重大利好。
React 17 中終於支持了原生捕獲事件的支持, 對齊了瀏覽器原生標準。
同時onScroll
事件再也不進行事件冒泡。onFocus
和 onBlur
使用原生 focusin
, focusout
合成。
Aligning with Browsers
We’ve made a couple of smaller changes related to the event system:
The onScroll event no longer bubbles to prevent common confusion.
React onFocus and onBlur events have switched to using the native focusin and focusout events under the hood, which more closely match React’s existing behavior and sometimes provide extra information.
Capture phase events (e.g. onClickCapture) now use real browser capture phase listeners.
官方的解釋是事件對象的複用在現代瀏覽器上性能已經提升的不明顯了,反而還很容易讓人用錯,因此乾脆就放棄這個優化。
參考
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!