React中的事件[擴展] - 非normal
這裏的事件指的是React內置的html組件中的事件
React官方當初在寫react的時候曾經思索。當用戶在react元素中綁定了事件以後, 如果react接管代碼以後在每一個真實的dom元素映射事件的話, 整個頁面中綁定的事件就會非常的多, 會影響整個頁面的執行效率, 所以經過深思熟慮以後, react團隊做出了以下的操作
- 用戶在react元素身上綁定事件以後, React會給document註冊事件
- 元素事件處理幾乎都在document的事件中處理(爲什麼說是幾乎, 因爲有些document上沒有的特殊事件就只能在元素上處理, 比如input框的onFocus和播放器的onPlay之類的, 可以去了解一下js知識)
- 在document的事件處理中, react會根據虛擬dom樹中結構完成事件函數的調用
我們來看一個有意思的例子
// 我們在App根組件中聲明一個react元素button, 並且給他註冊點擊事件, 同時我們又取到id爲root
// 的真實dom, 也給他註冊點擊事件, 按照邏輯來說, react元素button在root元素中, 那麼當我們點擊
// 按鈕的時候, 他會先觸發自身的點擊事件, 隨後冒泡到root點擊事件上導致root的點擊事件被觸發
export default class App extends React.PureComponent {
render() {
return (
<button onClick = { () => { console.log('我是react元素, 我被點擊了') } }>click</button>
)
}
}
document.querySelector('#root').addEventListener('click', () => {
console.log('我是真實dom - #root, 我被點擊了')
}, false)
上述操作輸出結果如下
我們再來看一個例子
import React from 'react';
// 組件A
function ChildA(props) {
return (
<div onClick = { () => {console.log('我是最外層的組件A')} }>我是最外層的組件A
<ChildB />
</div>
)
}
// 組件B
function ChildB(props) {
return (
<div onClick = { () => {console.log('我是中層的組件B')} }>我是最外層的組件B
<ChildC />
</div>
)
}
// 組件C
function ChildC(props) {
return (
<div onClick = { () => {console.log('我是最內層的組件C')} }>我是最內層的組件C</div>
)
}
export default class App extends React.PureComponent {
render() {
return (
<ChildA />
)
}
}
當我們點擊組件C的時候上述的操作輸出結果如下
我們清晰的發現, 第一個例子#root的點擊是輸出在button點擊之前的, 第二個例子卻看似正常, 這種怪異的現象是因爲什麼呢?
這下可以發現, 正是因爲react會將react內置元素的點擊事件都註冊在document身上, 並且形成隊列觸發, 那麼第一個例子的時候我們肯定點擊的先是#root, 然後冒泡上的document, 所以也可以解釋爲什麼第一個例子發生的奇怪的現象了
那麼如果我們在某個真實dom元素的註冊事件中直接取消事件冒泡, 那麼react元素綁定的同類型事件將不再觸發
總結
- 如果給真實的dom註冊了事件, 並且取消了事件冒泡, 那麼會導致react中相應的事件直接失效
- 如果給真實的dom註冊了事件, 事件會先於react事件運行
- react事件中的事件參數e並非真實的事件參數, 而是react進行合成後的對象
- 所以在react中的e.stopPropagation, 阻止的是虛擬dom中的事件冒泡, 而非真實dom的事件冒泡(實際上, 由於真實dom中react是給document綁定事件, 所以阻止document的冒泡毫無意義)
// 組件A
function ChildA(props) {
return (
<div onClick = { () => {console.log('我是最外層的組件A')} }>我是最外層的組件A
<ChildB />
</div>
)
}
// 當組件B組織了事件冒泡以後, 事件A的事件將不再被運行
function ChildB(props) {
return (
<div onClick = { (e) => {console.log('我是中層的組件B'); e.stopPropagation()} }>我是最外層的組件B
</div>
)
}
export default class App extends React.PureComponent {
render() {
return (
<ChildA />
)
}
}
- 通過e.nativeEvent來的到真實的事件e對象
- 如果我們將react混合其他的一些庫一起用, 在其他庫中也有可能利用事件委託給document註冊事件導致跟react衝突的話, 可以使用e.native.stopImmediatePropagation()來阻止剩餘的其他事件運行
- 爲了提高執行效率, react使用了事件對象池來處理事件對象, 也就是說在兩個事件處理函數中我們得到的e可能是同一個,(所謂同一個也就是地址是一個, 但是react會把這個對象中的屬性的值清空: 類似於type = null這種操作, 在下一次事件觸發的時候給屬性重新賦值)
// 我們定義一個prev來保存子組件B中的事件對象e, 然後拿到父組件a中的事件對象e跟prev進行比對
// 我們會發現prev === e 會返回true, 這就是react的事件池
let prev = null;
// 組件A
function ChildA(props) {
return (
<div onClick = { (e) => {
console.log('我是最外層的組件A')
console.log(prev, e, e === prev);
} }>我是父組件A
<ChildB />
</div>
)
}
// 組件B
function ChildB(props) {
return (
<div onClick = { (e) => {
console.log('我是中層的組件B');
console.log(e);
prev = e;
} }>
我是子組件B
</div>
)
}
export default class App extends React.PureComponent {
render() {
return (
<ChildA />
)
}
}
通過上面的例子, 所以我們一定要注意在事件處理程序中不要異步的使用事件對象e,如果我們一定想要異步使用, 那麼要在異步使用之前調用e.persist來進行持久化
// 如下操作, 可以被允許異步使用事件對象e
//...類組件...
render() {
return (
<button onClick = { (e) => {
e.presist();
setTimeout(() => {
console.log(e.type);
}, 1000)
} }></button>
)
}
//...