目錄
- 序言
- 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