「React 深入」React 事件系統與原生事件系統究竟有何區別?
大家好,我是小杜杜,我們知道React
自身提供了一套虛擬的事件系統,這套系統與原生系統到底有何區別?今天一起來詳細看看
作爲一個前端,我們會有很多的事件處理,所以學好React
事件系統是非常有必要的
關於
React事件系統
將會分三個方面去講解,分別是:React 事件系統與原生事件系統、深入 React v16 事件系統、對比 Reactv16~v18 事件系統 三個模塊,有感興趣的可以關注下新的專欄:React 深入進階,一起進階學習~
在正式開始前,讓我們一起來看看以下幾個問題:
-
React
爲什麼要單獨整一套虛擬的事件系統,這樣做有什麼好處? -
React
的事件系統,與原生的事件系統有什麼區別? -
什麼是合成事件?
-
React
中是如何模擬冒泡和捕獲階段的? -
所有的事件真的綁定在真實
DOM
上嗎?如果不是,那麼綁定在哪裏? -
React
中的阻止冒泡與原生事件系統的阻止冒泡一樣嗎,爲什麼? -
...
如果你能耐心的看完,相信一定能幫你更好的瞭解事件系統,先附上一張知識圖,供大家更好的觀看,還請各位小夥伴多多支持~
原生 DOM 事件
在講React的事件系統
前,我們先複習一下原生DOM事件
的概念,來幫助我們更好的理解
註冊事件
註冊事件:通過給元素添加點擊 (滾動等) 事件,稱作註冊事件,也叫綁定事件
註冊事件共有兩種方式,分別是:傳統註冊方式和監聽註冊方式
傳統註冊方式
傳統註冊方式:是以on
開頭的方式,完成註冊方式
如:
// 第一種
<button onclick="console.log(1)">點擊</button>
// 第二種
<button id="btn">點擊</button>
const btn = document.querySelector('#btn')
btn.onclick = function () {}
需要注意的是:我們註冊的事件都具備唯一性,也就是同一個元素只能設置一個
處理的函數,如果有多個,則會進行覆蓋
監聽註冊方式
監聽註冊方式:是以addEventListener
方法來監聽元素事件
addEventListener
方法並不支持IE 9
以下的瀏覽器,當有需要的時候可以使用attachEvent
方法,這個方法支持IE 10
以下的瀏覽器,但此方法建並非標準
如:
<button id="btn">點擊</button>
const btn = document.querySelector('#btn')
btn.addEventListener('click', () => {})
addEventListener
與傳統的方式不同,支持多次綁定事件,但要比傳統方式等級低
事件流
DOM
事件流共分爲三個階段:事件捕獲
、目標
和事件冒泡
三個階段
事件捕獲:由 DOM 最頂層節點開始,然後逐級向下傳播到最具體的元素接收的過程
事件冒泡:事件開始時由最具體的元素接收,然後逐級向上傳播到 DOM 最頂層節點的過程
特別注意:
-
JS 代碼中,只能執行
捕獲
或冒泡
其中的一個階段 -
addEventListener
的第三個參數爲false
代表冒泡(默認),爲true
代表捕獲 -
在真實的情況下,我們更多的關注是在
冒泡
上,可以利用冒泡
做一些很巧妙的事情,但有時又會帶來不必要的麻煩,應該合理的去利用 -
並不是所有的事件都有冒泡,有些事件並不存在冒泡事件,如:
onblur
、onfocus
、onmouseenter
事件等 -
合理的利用
e.stopPropagation()
、e.stopImmediatePropagation()
來阻止冒泡
擴展:阻止冒泡
這裏簡單介紹一下e.stopPropagation()
和e.stopImmediatePropagation()
的區別,方便大家更好的理解,
舉個栗子🌰:
<div id="id">
<button id="btn">點擊</button>
</div>
const div = document.querySelector('#id')
const btn = document.querySelector('#btn')
document.addEventListener('click', (e) => {
console.log(1)
})
div.addEventListener('click', (e) => {
console.log(2)
})
div.addEventListener('click', (e) => {
console.log(3)
})
div.addEventListener('click', (e) => {
console.log(4)
})
btn.addEventListener('click', () => {
console.log(5)
})
當我們點擊按鈕的時候,執行順序是:5 > 2 > 3 > 4 > 1,原因是執行了冒泡
,會從最底層的btn
開始執行,然後是div
,最後纔是頂層document
, 然後根據 Js 的執行順序,分別是 2、3 、4
那麼我們在console.log(3)
上加入e.stopPropagation()
看看,結果是什麼?
可以看到執行順序變成了:5 > 2 > 3 > 4
同樣的,我們換成e.stopImmediatePropagation()
來看看結果:
結論:e.stopImmediatePropagation()
相當於e.stopPropagation()
的增強版,不但可以阻止向上的冒泡,還能夠阻止同級
的擴散
擴展:獲取事件流的階段
我們如果想知道觸發事件的元素屬於三個階段的哪個階段時,可以通過e.eventPhase
來獲取
當e.eventPhase
爲 1 時,代表捕獲
階段,爲 2 時,代表目標
階段,爲 3 時,代表冒泡階段
事件委託
事件委託:也稱事件代理,在 JQ 中稱事件委派,也就是利用事件冒泡,將子級的事件委託給父級加載
也就是說,我們可以通過將監聽節點設置在父級上,然後利用冒泡來影響子集,如:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
const ul = document.querySelector('ul')
const lis = ul.children
ul.addEventListener('click', (e) => {
for(let i = 0; i < lis.length; i++) {
lis[i].style.background = ""
}
e.target.style.background = "red"
})
我們點擊對應的節點,點擊的節點需要高亮,如果設置在子級上,就需要監聽所有子級節點,就非常麻煩,此時我們可以監聽父級,來實現效果
初探 React 事件系統
點擊事件究竟去了哪?
我們先來看看原生 DOM 事件,用傳統 / 監聽註冊方式下,事件綁定在何處?
可以看出,原生 DOM 事件就綁定在對應的button
上,那麼 React 中,也是如此嗎?
我們發現在React
中,button
這個元素並沒有綁定事件,並且對應的點擊事件中有button
和document
兩個事件
先來看看button
事件,綁定的方法爲nonp
然而綁定的 nonp
只是一個空函數,也就說,真正的事件綁定到了document
上
點擊事件究竟存儲到了哪?
之前在「React 深入」一文喫透虛擬 DOM 和 diff 算法中講過,我們編寫的jsx
代碼首先會被babel
轉化爲React.createElement
,最終被轉化爲fiber
對象
接下來我們逐步看看上述的代碼轉化後的樣子:
React.createElement
形式:
fiber
對象形式(可以選中當前元素,然後輸入console.dir($0)
查看當前元素):
可以發現,事件最終保存在fiber
中的memoizedProps
和 pendingProps
中
什麼是合成事件?
合成事件(SyntheticEvent):是React
模擬原生 DOM 事件所有能力的一個事件對象,即瀏覽器原生事件的跨瀏覽器包裝器
簡單的來講,我們在上述的例子中,使用onClick
點擊事件,而onClick
並不是原生事件,而是由原生事件合成的React
事件
爲了更好的理解,我們來看看input
的onChang
事件
export default function App(props) {
return (
<div>
<input onChange={(e) => console.log(e)}></input>
</div>
);
}
此時在doncument
上的事件爲
也就是說,我們綁定的onChange
事件最終被處理成了很多事件的監聽器,如:blur
、change
、focus
、input
等
爲什麼要單獨整一套呢?
React
爲什麼要採取事件合成
的模式呢?這樣做有什麼好處呢?
兼容性,跨平臺
我們知道有多種瀏覽器,每個瀏覽器的內核都不相同,而React
通過頂層事件代理機制,保證冒泡的統一性,抹平不同瀏覽器事件對象之間的差異,將不同平臺的事件進行模擬成合成事件,使其能夠跨瀏覽器執行
將所有事件統一管理
在原生事件中,所有的事件都綁定在對應的Dom
上,如果頁面複雜,綁定的事件會非常多,這樣就會造成一些不可控的問題
而React
將所有的事件都放在了document
上,這樣就可以對事件進行統一管理
,從而避免一些不必要的麻煩
避免垃圾回收
我們來看看React
和原生事件
中的input
都綁定onChange
事件是什麼樣子?
可以看出,原生事件綁定onchange
對應的就是change
,而React
會被處理爲很多的監聽器
在實際中,我們的事件會被頻繁的創建和回收,這樣會影響其性能,爲了解決這個問題,React
引入事件池,通過事件池來獲取和釋放事件。
也就是說,所有的事件並不會被釋放,而是存入到一個數組中,如果這個事件觸發,則直接在這個數組中彈出即可,這樣就避免了頻繁創建和銷燬
淺談合成事件與原生事件
執行順序對比
我們先模擬下React
中的合成事件和原生中的事件順序,如:
import React, {useEffect, useRef} from "react";
export default function App(props) {
const ref = useRef(null)
const ref1 = useRef(null)
useEffect(() => {
const div = document.querySelector("div")
const button = document.querySelector("button")
div.addEventListener("click", () => console.log("原生:div元素"))
button.addEventListener("click", () => console.log("原生:button元素"))
document.addEventListener("click", () => console.log("原生:document元素"))
}, [])
return (
<div onClick={() => console.log('React:div元素')}>
<button
onClick={() => console.log('React:按鈕元素')}
>
執行順序
</button>
</div>
);
}
執行結果:
由上圖可看出,當DOM
(button) 元素觸發後,會先執行原生事件
,再處理React
時間,最後真正執行document
上掛載的事件
與原生事件有何不同?
事件名不同
-
原生事件,是以
純小寫
來命名,如:onclick
-
合成事件,是以
小駝峯式
來命名,如:onClick
接受的參數不同
-
原生事件,接受的參數是字符串,如:
Click()
-
合成事件,接受的參數是函數,如:
Click()
事件源不同,阻止默認事件的方式不同
在React
中,我們的所有事件都可以說是虛擬
的,並不是原生的事件
我們在React
中拿到的事件源 (e) 也並非是真正的事件e
,而是經過React
單獨處理的e
-
原生事件中,可以通過
e.preventDefault()
和return false
來阻止默認事件 -
合成事件中,通過
e.preventDefault()
阻止默認事件
“
特別注意,原生事件和合成時間的
e.preventDefault()
並非是同一個函數,React 的事件源 e 是單獨創立的,所以兩者的方法也不相同,同時return false
也在React
中無效”
擴展:對比 e.stopPropagation() 和 e.nativeEvent.stopImmediatePropagation
來擴展下阻止冒泡的方法:
爲了更好的說明,我們分別使用e.stopPropagation()
和 e.nativeEvent.stopImmediatePropagation
有什麼效果:
正常觸發:
e.stopPropagation()
觸發:
e.nativeEvent.stopImmediatePropagation
觸發:
從上圖可知:
-
e.stopPropagation():可以阻止當前
DOM
事件的冒泡,但事實上,e.stopPropagation()
只能阻止合成事件的冒泡,即不會阻止頂流document
上 -
e.nativeEvent.stopImmediatePropagation:於
e.stopPropagation()
正好相反,只能阻止綁定在document
上的監聽事件
擴展:冒泡和捕獲階段
在React
中,所有的綁定事件(如:onClick
、onChange
)都是冒泡階段執行。
所有的捕獲階段統一加Capture
,如onClickCapture
、onChangeCapture
舉個小例子:
<button
onClick={() => {console.log('冒泡')}}
onClickCapture={() => {console.log("捕獲")}} >
點擊
</button>
是否可以混用?
我們通過對比原生事件和合成事件後,提出一個疑問,原生事件和合成事件是否可以一起使用?
先來舉個栗子🌰,一起看看
import React, {useEffect, useRef} from "react";
export default function App(props) {
useEffect(() => {
const button = document.querySelector("button")
button.addEventListener("click", (e) => {
e.stopPropagation();
console.log("原生button阻止冒泡");
})
document.addEventListener("click", () => {
console.log("原生document元素");
});
}, [])
return (
<button
onClick={() => {
console.log('按鈕事件')
}}
>
混用
</button>
);
}
結果:
可以發現只執行了原生事件,並沒有執行合成事件,這是因爲原生事件的執行順序在合成事件之前,所以導致合成事件沒有辦法進行註冊。
所以兩者建議不要進行混用,否則會跳過React
的事件機制
End
參考
- 官網:合成事件
結語
本文通過對比React 事件系統
和原生事件系統
,詳細的瞭解兩者的區別,實際上React
上的事件都綁定在了document
上,就連事件源也並非是原生中的事件源
那麼,React
究竟如何綁定事件的,又是如何觸發事件的?爲什麼我們必須通過this
去綁定對應的事件?又是如何處理批量更新的?... 都是一些我們值得探討的問題。
感興趣的可以關注下這個專欄,這個專欄會以進階爲目的,詳細講解React
相關的原理、源碼、實戰,有感興趣的可以關注下,一起學習,一起進步~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pXqWahbpIFbz3ynIOsGgGg