目錄
- 序言
- DOM事件流
- 事件捕獲階段、處於目標階段、事件冒泡階段
- addEventListener 方法
- React 事件概述
- 事件註冊
- document 上註冊
- 回撥函式儲存
- 事件分發
- 小結
- 參考
1.序言
React 有一套自己的事件系統,其事件叫做合成事件。為什麼 React 要自定義一套事件系統?React 事件是如何註冊和觸發的?React 事件與原生 DOM 事件有什麼區別?帶著這些問題,讓我們一起來探究 React 事件機制的原理。為了便於理解,此篇分析將盡可能用圖解代替貼 React 原始碼進行解析。
2.DOM事件流
首先,在正式講解 React 事件之前,有必要了解一下 DOM 事件流,其包含三個流程:事件捕獲階段、處於目標階段和事件冒泡階段。
W3C協會早在1988年就開始了DOM標準的制定,W3C DOM標準可以分為 DOM1、DOM2、DOM3 三個版本。
從 DOM2 開始,DOM 的事件傳播分三個階段進行:事件捕獲階段、處於目標階段和事件冒泡階段。
(1)事件捕獲階段、處於目標階段和事件冒泡階段
示例程式碼:
<html>
<body>
<div id="outer">
<p id="inner">Click me!</p>
</div>
</body>
</html>
上述程式碼,如果點選 <p>
元素,那麼 DOM 事件流如下圖:
(1)事件捕獲階段:事件物件通過目標節點的祖先 Window 傳播到目標的父節點。
(2)處於目標階段:事件物件到達事件目標節點。如果阻止事件冒泡,那麼該事件物件將在此階段完成後停止傳播。
(3)事件冒泡階段:事件物件以相反的順序從目標節點的父項開始傳播,從目標節點的父項開始到 Window 結束。
(2)addEventListener 方法
DOM 的事件流中同時包含了事件捕獲階段和事件冒泡階段,而作為開發者,我們可以選擇事件處理函式在哪一個階段被呼叫。
addEventListener() 方法用於為特定元素繫結一個事件處理函式。addEventListener 有三個引數:
element.addEventListener(event, function, useCapture)
另外,如果一個元素(element)針對同一個事件型別(event),多次繫結同一個事件處理函式(function),那麼重複的例項會被拋棄。當然如果第三個引數capture
值不一致,此時就算重複定義,也不會被拋棄掉。
3.React 事件概述
React 根據W3C 規範來定義自己的事件系統,其事件被稱之為合成事件 (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 上註冊、儲存事件回撥。
(1)document 上註冊
在 React 元件掛載階段,根據元件內的宣告的事件型別(onclick、onchange 等),在 document 上註冊事件(使用addEventListener),並指定統一的回撥函式 dispatchEvent。換句話說,document 上不管註冊的是什麼事件,都具有統一的回撥函式 dispatchEvent。也正是因為這一事件委託機制,具有同樣的回撥函式 dispatchEvent,所以對於同一種事件型別,不論在 document 上註冊了幾次,最終也只會保留一個有效例項,這能減少記憶體開銷。
示例程式碼:
function TestComponent() {
handleFatherClick=()=>{
// ...
}
handleChildClick=()=>{
// ...
}
return <div className="father" onClick={this.handleFatherClick}>
<div className="child" onClick={this.handleChildClick}>child </div>
</div>
}
上述程式碼中,事件型別都是onclick
,由於 React 的事件委託機制,會指定統一的回撥函式 dispatchEvent,所以最終只會在 document 上保留一個 click 事件,類似document.addEventListener('click', dispatchEvent)
,從這裡也可以看出 React 的事件是在 DOM 事件流的冒泡階段被觸發執行。
(2)儲存事件回撥
React 為了在觸發事件時可以查詢到對應的回撥去執行,會把元件內的所有事件統一地存放到一個物件中(listenerBank)。而儲存方式如上圖,首先會根據事件型別分類儲存,例如 click 事件相關的統一儲存在一個物件中,回撥函式的儲存採用鍵值對(key/value)的方式儲存在物件中,key 是元件的唯一標識 id,value 對應的就是事件的回撥函式。
React 的事件註冊的關鍵步驟如下圖:
5.事件分發
事件分發也就是事件觸發。React 的事件觸發只會發生在 DOM 事件流的冒泡階段,因為在 document 上註冊時就預設是在冒泡階段被觸發執行。
其大致流程如下:
- 觸發事件,開始 DOM 事件流,先後經過三個階段:事件捕獲階段、處於目標階段和事件冒泡階段
- 當事件冒泡到 document 時,觸發統一的事件分發函式
ReactEventListener.dispatchEvent
- 根據原生事件物件(nativeEvent)找到當前節點(即事件觸發節點)對應的 ReactDOMComponent 物件
- 事件的合成
- 根據當前事件型別生成對應的合成物件
- 封裝原生事件物件和冒泡機制
- 查詢當前元素以及它所有父級
- 在 listenerBank 中查詢事件回撥函式併合成到 events 中
- 批量執行合成事件(events)內的回撥函式
- 如果沒有阻止冒泡,會將繼續進行 DOM 事件流的冒泡(從 document 到 window),否則結束事件觸發
注:上圖中阻止冒泡
是指呼叫stopImmediatePropagation
方法阻止冒泡,如果是呼叫stopPropagation
阻止冒泡,document 上如果還註冊了同型別其他的事件,也將會被觸發執行,但會正常阻斷 window 上事件觸發。瞭解兩者之間的詳細區別
示例程式碼:
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 (
<div class='parent' onClick={this.parentClick} ref={ref => this.parent = ref}>
<div class='child' onClick={this.childClick} ref={ref => this.child = ref}>
Click me!
</div>
</div>)
}
}
點選 child div 時,其輸出如下:
在 DOM 事件流的冒泡階段先後經歷的元素:child <div>
-> parent <div>
-> <body>
-> <html>
-> 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級事件模型解析
Event dispatch and DOM event flow