初步了解
表象了解
先回顧一下 React 事件機制基本了解,React 自身實作了一套自己的事件機制,包括事件注冊、事件合成、事件冒泡、事件派發等,雖然和原生是兩碼事,但是也是基于浏覽器的事件機制下完成的。
我們都知道 React 的所有事件并沒有綁定到具體的 DOM 節點,而是綁定到 document 上,然後由統一的事件處理程式來處理,同時也是基于浏覽器的事件機制(冒泡),所有節點的時間都會在 document 上觸發。
試想一下
如果一個節點同時綁定了合成和原生事件,那麼禁止冒泡後執行關系是怎樣?
因為合成事件的觸發是基于浏覽器的事件機制來實作的,通過冒泡機制冒泡到最頂層元素,然後再由 dispatchEvent 統一去處理。
得出結論:
原生事件阻止冒泡肯定會組織合成事件的觸發,合成事件的阻止冒泡不會影響原生事件。
原因在于,浏覽器的事件執行機制是執行在前,冒泡在後,是以在原生事件中阻止冒泡會阻止合成事件的執行,反之不成立。
綜上,兩者最好不要一起使用,避免出現一些奇怪的問題。
意義
React 将事件全部統一交給 document 來委托處理的原因是:
- 減少記憶體消耗,提高性能,不需要注冊那麼多的事件,一種事件隻需要在 document 上注冊一次即可
- 統一規範,用于解決相容性問題,簡化事件邏輯
- 對開發者更加友好
對合成的了解
既然我們對 React 的事件機制有了初步的了解,那麼可以知道合成事件并不是簡單的合成和處理,從廣義上還包括:
- 對原生事件的封裝
- 對某些原生事件的更新和改造
- 不同浏覽器事件的相容處理
對原生事件的封裝
img
上面的代碼是個一個元素添加點選事件的回調函數,方法中的參數 e 其實并不是原生事件中的 event,而是 React 包裝過的對象,同時原生事件中的 event 被放在了這個對象的 nativeEvent 字段。
img
再看下官網文檔
img
SyntheticEvent 是 React 合成事件的基類,定義了合成事件的基礎公共屬性和方法。
React 會根據目前的事件類型來使用不同的合成事件對象,比如滑鼠:點選事件 -- SyntheticMouseEvent,焦點事件 -- SyntheticFocusEvent 等,但都是繼承與 SyntheticEvent。
img
img
img
對原生事件的更新和改造
對于有些 DOM 元素事件,我們進行事件綁定之後,Reacgt 并不是隻處理你生命的事件類型,還會額外增加一些其他的事件,幫助我們提升互動和體驗。
比如說:
當我們給 input 生命一個 onChange 事件,React 幫我們做了很多工作:
img
可以看到 React 不隻是幫助我們注冊一個 onchange 事件,還注冊了很多其他的事件。
而這時候我們向文本框輸入内容的時候,是可以實時得到内容,
然而原生事件隻注冊了一個 onchange 的話,需要在失去焦點的時候才能觸發這個事件,這個缺陷 React 幫我們彌補了。
ps:圖中有一個 invalid 事件是注冊在目前元素而非在 document 的,可能是因為這個事件是 HTML5 表單屬性特有的,需要在輸入框輸入的時候進行校驗,如果是放到 document 上就不會生效了。
浏覽器的相容處理
react 在給 document 注冊事件的時候也是做了相容性處理的。
img
上面這個代碼就可以看出,在給 document 注冊事件的時候,内部也同時對 IE 浏覽器做了相容處理。
事件注冊機制
大緻流程
React 事件注冊其實主要做了兩件事情:
- 事件注冊:元件挂載階段,根據元件内聲明的事件類型:onclick、onchange 等,給 document 添加事件監聽,并制定統一的事件處理程式 dispatchEvent;
- 事件存儲:就是把 React 元件内所有事件統一存放到一個對象内,緩存起來,為了在觸發事件的時候能夠找到對應的方法去執行。
img
關鍵步驟
首先 React 拿到将要挂載在元件的虛拟 DOM(React Element 對象),然後處理 React DOM 的 props,判斷屬性内是否有聲明為事件的屬性,比如 onClick、onChange 等,這個時候得到事件類型 click、change 等和與之對應的回調函數,然後執行後面三步:
- 完成事件注冊
- 将 React DOM、事件類型、回調函數放入數組存儲
- 元件挂載完成後,處理步驟2生成的數組,經過周遊把事件回調函數存儲到 **listenerBank(一個對象)**中。
img
源碼解析
從 jsx 說起
//...省略
經過 babel 編譯之後,我們看到最終調用方法是
React.createElement
,而且生命的事件類型和回調函數就是個 props
img
React.createElement
執行的結果會傳回一個所謂的虛拟DOM(React Element Object)。
img
處理元件 props,拿到事件類型和回調函數
ReactDOMComponent 在進行元件加載(mount)、更新(update)的時候,需要對 props 進行處理(_updateDOMProperties):
img
可以看下 registrationNameModules 裡面的内容,就是一個内置的常量:
img
事件注冊和事件的存儲
事件注冊
接着上面的代碼執行到了這個方法
declare
在這個方法會進行事件的注冊以及事件的存儲,包括冒泡和捕獲的處理
img
根據目前的元件執行個體擷取到最高父級,也就是 document,然後執行方法 listenTo,也是另一個很關鍵的方法進行事件綁定處理。
img
最後執行
EventListener.listen
(冒泡)或者
EventListener.capture
(捕獲),但看下冒泡的注冊,其實就是
addEventListener
第三個參數設定為 false。
img
同時我們看到這裡也同樣對 IE 浏覽器做了相容。
上面沒有看到 dispatchEvent 的定義,下面可以看到傳入 dispatchEvent 方法的代碼。
img
到這裡事件注冊就完成了。
事件存儲
開始事件的存儲,在 React 裡所有事件的觸發都是通過 dispatchEvent 方法統一進行派發的,而不是在注冊的時候直接注冊聲明的回調。
React 把所有的事件和事件類型以及 React 元件進行關聯,把這個關系儲存在一個 Map 裡面,然後在事件觸發的時候根據目前的元件id 和事件類型找到對應的事件的回調函數。
img
綜合源碼:
大緻的流程是執行完 listenTo(事件注冊),然後執行 putListener 方法進行事件存儲,所有的事件都會存儲到一個對象中 -- listenerBank,具體由 EventPluginHub 進行管理。
//拿到元件唯一辨別 id
listenerBank 其實就是一個二級 Map,這樣的結構更加友善事件的查找。
這裡的元件id 就是元件的唯一标志,然後和 fn 進行關聯,再觸發階段就可以找到相關的事件回調。
img
沒看錯,雖然我一直稱呼為 Map,但其實就是一個我們平常使用的 object。
補充一個詳細的完整流程圖:
img
事件執行階段
在事件注冊階段,最終所有的事件和事件類型都會儲存到 listenerBank 中。
再觸發階段,我們通過這個對象進行事件的查找,然後執行回調函數。
大緻流程
- 進入統一的事件分發函數(dispatchEvent)
- 結合原生事件找到目前節點對應的 ReactDOMComponent 對象
- 開始事件的合成
- 根據目前事件類型生成指定的合成對象
- 封裝原生事件和冒泡機制
- 查找目前元素以及它所有父級
- 在 listenerBank 查找事件回調并合成到 event(合成事件結束)
- 批量處理合成事件内的回調函數(事件觸發完成)
img
舉個例子
//...省略
當我們點選 child div 的時候,會同時觸發 father 的事件
img
源碼解析
dispatchEvent 進行事件分發
進入統一的事件分發函數(dispatchEvent)。
當我點選 child div 的時候,這個時候浏覽器會捕獲到這個事件,然後經過冒泡,事件會冒泡到 document 上,交給統一事件處理函數 dispatchEvent 進行處理。
img
查找 ReactDOMComponent
結合原生事件找到目前節點對應的 ReactDOMComponent 對象,在原生事件對象内已經保留了對應的 ReactDOMComponent 執行個體引用,應該是在挂載階段就已經儲存。
img
看下 ReactDOMComponent 執行個體的内容:
img
合成事件ing
事件的合成,冒泡的處理以及事件回調的查找都是在合成階段完成的。
img
合成對象的生成
根據目前事件類型找到對應的合成類,然後進行合成對象的生成
//進行事件合成,根據事件類型獲得指定的合成類
封裝原生事件和冒泡機制
在這一步會把原生事件對象挂載到合成對象的自身,同時增加事件的預設行為處理和冒泡機制。
/**
*
* @param {obj} dispatchConfig 一個配置對象 包含目前的事件依賴 ["topClick"],冒泡和捕獲事件對應的名稱 bubbled: "onClick",captured: "onClickCapture"
* @param {obj} targetInst 元件執行個體ReactDomComponent
* @param {obj} nativeEvent 原生事件對象
* @param {obj} nativeEventTarget 事件源 e.target = div.child
*/
下面是增加的預設行為和冒泡機制的處理方法,其實就是改變了目前合成對象的屬性值,調用了方法後屬性值為 true,就會組織預設行為或者冒泡。
//在合成類原型上增加preventDefault和stopPropagation方法
列印一下 emptyFunction 代碼
img
查找所有父級執行個體
根據目前節點實力查找他的所有父級執行個體,并存入 path
/**
*
* @param {obj} inst 目前節點執行個體
* @param {function} fn 處理方法
* @param {obj} arg 合成事件對象
*/
path 就是一個數組,裡面的元素是 ReactDOMComponent
img
合成事件結束
在 listenerBank 查找事件回調并合成到 event。
緊接着上面的代碼
'bubbled', arg);
上面的代碼會調用下面這個方法,在 listenerBank 中查找到事件回調,并存入合成事件對象。
img
為什麼能夠查找到的呢?
因為 inst (元件執行個體)裡有_rootNodeID,是以也就有了對應關系。
img
到這裡,合成事件對象生成完成,所有的事件回調一儲存到合成對象中。
批量處理事件合成對象
批量處理合成事件對象内的回調方法。
生成完合成事件對象後,調用棧回到了我們起初執行的方法内。
img
//在這裡執行事件的回調
img
到下面這一步中間省略了一些代碼,隻貼出主要的代碼,下面方法會循環處理 合成事件内的回調方法,同時判斷是否禁止事件冒泡。
img
貼上最後的執行回調方法的代碼
/**
*
* @param {obj} event 合成事件對象
* @param {boolean} simulated false
* @param {fn} listener 事件回調
* @param {obj} inst 元件執行個體
*/
img
最後react 通過生成了一個臨時節點fakeNode,然後為這個臨時元素綁定事件處理程式,然後建立自定義事件 Event,通過fakeNode.dispatchEvent方法來觸發事件,并且觸發完畢之後立即移除監聽事件。
到這裡事件回調已經執行完成,但是也有些疑問,為什麼在非生産環境需要通過自定義事件來執行回調方法。可以看下上面的代碼在非生産環境對 ReactErrorUtils.invokeGuardedCallback方法進行了重寫。