深入理解React:事件機制原理
**1.序言** React 有一套自己的事件系統,其事件叫做[合成事件](https://zh-hans.reactjs.org/docs/events.html)。為什麼 React 要自定義一套事件系統?React 事件是如何註冊和觸發的?React 事件與原生 DOM 事件有什麼區別?帶著這些問題,讓我們一起來探究 React 事件機制的原理。為了便於理解,此篇分析將盡可能用圖解代替貼 React 原始碼進行解析。
**2.DOM事件流** 首先,在正式講解 React 事件之前,有必要了解一下 DOM 事件流,其包含三個流程:事件捕獲階段、處於目標階段和事件冒泡階段。 > W3C協會早在1988年就開始了DOM標準的制定,W3C DOM標準可以分為 DOM1、DOM2、DOM3 三個版本。 > > 從 DOM2 開始,DOM 的事件傳播分三個階段進行:事件捕獲階段、處於目標階段和事件冒泡階段。
**(1)事件捕獲階段、處於目標階段和事件冒泡階段** 示例程式碼: ```html
Click me!
`元素,那麼 DOM 事件流如下圖:
![](https://img2020.cnblogs.com/blog/898684/202006/898684-20200624143429777-917104228.png)
(1)事件捕獲階段:事件物件通過目標節點的祖先 Window 傳播到目標的父節點。
(2)處於目標階段:事件物件到達事件目標節點。如果阻止事件冒泡,那麼該事件物件將在此階段完成後停止傳播。
(3)事件冒泡階段:事件物件以相反的順序從目標節點的父項開始傳播,從目標節點的父項開始到 Window 結束。
**(2)addEventListener 方法**
DOM 的事件流中同時包含了事件捕獲階段和事件冒泡階段,而作為開發者,我們可以選擇事件處理函式在哪一個階段被呼叫。
**addEventListener()** 方法用於為特定元素繫結一個事件處理函式。addEventListener 有三個引數:
```js
element.addEventListener(event, function, useCapture)
```
![](https://img2020.cnblogs.com/blog/898684/202006/898684-20200624143450007-605465502.png)
另外,如果一個元素(element)針對同一個事件型別(event),多次繫結同一個事件處理函式(function),那麼重複的例項會被拋棄。當然如果第三個引數`capture`值不一致,此時就算重複定義,也不會被拋棄掉。
**3.React 事件概述**
React 根據[W3C 規範](https://www.w3.org/TR/DOM-Level-3-Events/)來定義自己的事件系統,其事件被稱之為合成事件 (SyntheticEvent)。而其自定義事件系統的動機主要包含以下幾個方面:
(1)**抹平不同瀏覽器之間的相容性差異**。最主要的動機。
(2)**事件"合成",即事件自定義**。事件合成既可以處理相容性問題,也可以用來自定義事件(例如 React 的 onChange 事件)。
(3)**提供一個抽象跨平臺事件機制**。類似 VirtualDOM 抽象了跨平臺的渲染方式,合成事件(SyntheticEvent)提供一個抽象的跨平臺事件機制。
(4)**可以做更多優化**。例如利用事件委託機制,幾乎所有事件的觸發都代理到了 document,而不是 DOM 節點本身,簡化了 DOM 事件處理邏輯,減少了記憶體開銷。(React 自身模擬了一套事件冒泡的機制)
(5)**可以干預事件的分發**。V16引入 Fiber 架構,React 可以通過干預事件的分發以優化使用者的互動體驗。
*注:「幾乎」所有事件都代理到了 document,說明有例外,比如`audio`、`video`標籤的一些媒體事件(如 onplay、onpause 等),是 document 所不具有,這些事件只能夠在這些標籤上進行事件進行代理,但依舊用統一的入口分發函式(dispatchEvent)進行繫結。*
**4.事件註冊**
React 的事件註冊過程主要做了兩件事:document 上註冊、儲存事件回撥。
![](https://img2020.cnblogs.com/blog/898684/202006/898684-20200624143507392-911347960.png)
(1)document 上註冊
在 React 元件掛載階段,根據元件內的宣告的事件型別(onclick、onchange 等),在 document 上註冊事件(使用addEventListener),並指定統一的回撥函式 dispatchEvent。換句話說,document 上不管註冊的是什麼事件,都具有統一的回撥函式 dispatchEvent。也正是因為這一事件委託機制,具有同樣的回撥函式 dispatchEvent,所以對於同一種事件型別,不論在 document 上註冊了幾次,最終也只會保留一個有效例項,這能減少記憶體開銷。
示例程式碼:
```jsx
function TestComponent() {
handleFatherClick=()=>{
// ...
}
handleChildClick=()=>{
// ...
}
return
(2)儲存事件回撥 React 為了在觸發事件時可以查詢到對應的回撥去執行,會把元件內的所有事件統一地存放到一個物件中(listenerBank)。而儲存方式如上圖,首先會根據事件型別分類儲存,例如 click 事件相關的統一儲存在一個物件中,回撥函式的儲存採用鍵值對(key/value)的方式儲存在物件中,key 是元件的唯一標識 id,value 對應的就是事件的回撥函式。
React 的事件註冊的關鍵步驟如下圖: ![](https://img2020.cnblogs.com/blog/898684/202006/898684-20200624143524310-1842672426.png)
**5.事件分發** 事件分發也就是事件觸發。React 的事件觸發只會發生在 DOM 事件流的冒泡階段,因為在 document 上註冊時就預設是在冒泡階段被觸發執行。
其大致流程如下: 1. 觸發事件,開始 DOM 事件流,先後經過三個階段:事件捕獲階段、處於目標階段和事件冒泡階段 2. 當事件冒泡到 document 時,觸發統一的事件分發函式 `ReactEventListener.dispatchEvent` 3. 根據原生事件物件(nativeEvent)找到當前節點(即事件觸發節點)對應的 ReactDOMComponent 物件 4. 事件的合成 1. 根據當前事件型別生成對應的合成物件 2. 封裝原生事件物件和冒泡機制 3. 查詢當前元素以及它所有父級 4. 在 listenerBank 中查詢事件回撥函式併合成到 events 中 5. 批量執行合成事件(events)內的回撥函式 6. 如果沒有阻止冒泡,會將繼續進行 DOM 事件流的冒泡(從 document 到 window),否則結束事件觸發 ![](https://img2020.cnblogs.com/blog/898684/202006/898684-20200624143540789-1118047434.png) 注:上圖中`阻止冒泡`是指呼叫`stopImmediatePropagation` 方法阻止冒泡,如果是呼叫`stopPropagation`阻止冒泡,document 上如果還註冊了同類型其他的事件,也將會被觸發執行,但會正常阻斷 window 上事件觸發。[瞭解兩者之間的詳細區別](https://my.oschina.net/i33/blog/84981)
示例程式碼: ```jsx class TestComponent extends React.Component { componentDidMount() { this.parent.addEventListener('click', (e) => { console.log('dom parent'); }) this.child.addEventListener('click', (e) => { console.log('dom child'); }) document.addEventListener('click', (e) => { console.log('document'); }) document.body.addEventListener('click', (e) => { console.log('body'); }) window.addEventListener('click', (e) => { console.log('window'); }) } childClick = (e) => { console.log('react child'); } parentClick = (e) => { console.log('react parent'); } render() { return ( this.parent = ref}> this.child = ref}> Click me! ) } } ``` 點選 child div 時,其輸出如下: ![](https://img2020.cnblogs.com/blog/898684/202006/898684-20200624143557043-627913590.png) 在 DOM 事件流的冒泡階段先後經歷的元素:`child ` -> `parent ` -> `` -> `` -> `document` -> `window`,因此上面的輸出符合預期。
**6.小結** **React 合成事件和原生 DOM 事件的主要區別:** (1)React 元件上宣告的事件沒有繫結在 React 元件對應的原生 DOM 節點上。 (2)React 利用事件委託機制,將幾乎所有事件的觸發代理(delegate)在 document 節點上,事件物件(event)是合成物件(SyntheticEvent),不是原生事件物件,但通過 nativeEvent 屬性訪問原生事件物件。 (3)由於 React 的事件委託機制,React 元件對應的原生 DOM 節點上的事件觸發時機總是在 React 元件上的事件之前。
**7.參考** [javascript中DOM0,DOM2,DOM3級事件模型解析](http://www.webzsky.com/?p=731) [Event dispatch and DOM event flow](https://www.w3.org/TR/DOM-Level-3-Events/#event-flow) [EventTarget.addEventListener() - Web API 介面參考| MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener) [合成事件](https://zh-hans.reactjs.org/docs/events.html) [談談React事件機制和未來(react-events)](https://bobi.ink/2019/07/29/react-event) [React原始碼解讀系列 – 事件機制](http://zhenhua-lee.github.io/react/react-event.html) [一文吃透 react 事件機制原理](https://cloud.tencent.com/developer/article/1516369)