關於React事件的疑問
-
1.為什麼要手動繫結
this
-
2.
React
事件和原生事件有什麼區別 -
3.
React
事件和原生事件的執行順序,可以混用嗎 -
4.
React
事件如何解決跨瀏覽器相容 -
5.什麼是合成事件
下面是我閱讀過原始碼後,將所有的執行流程總結出來的流程圖,不會貼程式碼,如果你想閱讀程式碼看看具體是如何實現的,可以根據流程圖去原始碼裡尋找。
事件註冊
- 元件裝載 / 更新。
- 通過
lastProps
、nextProps
判斷是否新增、刪除事件分別呼叫事件註冊、解除安裝方法。 - 呼叫
EventPluginHub
的enqueuePutListener
進行事件儲存 - 獲取
document
物件。 - 根據事件名稱(如
onClick
、onCaptureClick
)判斷是進行冒泡還是捕獲。 - 判斷是否存在
addEventListener
方法,否則使用attachEvent
(相容IE)。 - 給
document
註冊原生事件回撥為dispatchEvent
(統一的事件分發機制)。
事件儲存
EventPluginHub
負責管理React合成事件的callback
,它將callback
儲存在listenerBank
中,另外還儲存了負責合成事件的Plugin
。EventPluginHub
的putListener
方法是向儲存容器中增加一個listener。- 獲取繫結事件的元素的唯一標識
key
。 - 將
callback
根據事件型別,元素的唯一標識key
儲存在listenerBank
中。 listenerBank
的結構是:listenerBank[registrationName][key]
。
例如:
{
onClick:{
nodeid1:()=>{...}
nodeid2:()=>{...}
},
onChange:{
nodeid3:()=>{...}
nodeid4:()=>{...}
}
}
複製程式碼
事件觸發 / 執行
這裡的事件執行利用了React
的批處理機制,在前一篇的【React深入】setState的執行機制中已經分析過,這裡不再多加分析。
- 觸發
document
註冊原生事件的回撥dispatchEvent
- 獲取到觸發這個事件最深一級的元素
例如下面的程式碼:首先會獲取到this.child
<div onClick={this.parentClick} ref={ref => this.parent = ref}>
<div onClick={this.childClick} ref={ref => this.child = ref}>
test
</div>
</div>
複製程式碼
- 遍歷這個元素的所有父元素,依次對每一級元素進行處理。
- 構造合成事件。
- 將每一級的合成事件儲存在
eventQueue
事件佇列中。 - 遍歷
eventQueue
。 - 通過
isPropagationStopped
判斷當前事件是否執行了阻止冒泡方法。 - 如果阻止了冒泡,停止遍歷,否則通過
executeDispatch
執行合成事件。 - 釋放處理完成的事件。
react
在自己的合成事件中重寫了stopPropagation
方法,將isPropagationStopped
設定為true
,然後在遍歷每一級事件的過程中根據此遍歷判斷是否繼續執行。這就是react
自己實現的冒泡機制。
合成事件
- 呼叫
EventPluginHub
的extractEvents
方法。 - 迴圈所有型別的
EventPlugin
(用來處理不同事件的工具方法)。 - 在每個
EventPlugin
中根據不同的事件型別,返回不同的事件池。 - 在事件池中取出合成事件,如果事件池是空的,那麼建立一個新的。
- 根據元素
nodeid
(唯一標識key
)和事件型別從listenerBink
中取出回撥函式 - 返回帶有合成事件引數的回撥函式
總流程
將上面的四個流程串聯起來。
為什麼要手動繫結this
通過事件觸發過程的分析,dispatchEvent
呼叫了invokeGuardedCallback
方法。
function invokeGuardedCallback(name, func, a) {
try {
func(a);
} catch (x) {
if (caughtError === null) {
caughtError = x;
}
}
}
複製程式碼
可見,回撥函式是直接呼叫呼叫的,並沒有指定呼叫的元件,所以不進行手動繫結的情況下直接獲取到的this
是undefined
。
這裡可以使用實驗性的屬性初始化語法 ,也就是直接在元件宣告箭頭函式。箭頭函式不會建立自己的this
,它只會從自己的作用域鏈的上一層繼承this
。因此這樣我們在React
事件中獲取到的就是元件本身了。
和原生事件有什麼區別
-
React
事件使用駝峰命名,而不是全部小寫。 -
通過
JSX
, 你傳遞一個函式作為事件處理程式,而不是一個字串。
例如,HTML
:
<button onclick="activateLasers()">
Activate Lasers
</button>
複製程式碼
在 React
中略有不同:
<button onClick={activateLasers}>
Activate Lasers
</button>
複製程式碼
另一個區別是,在 React 中你不能通過返回false
來阻止預設行為。必須明確呼叫 preventDefault
。
由上面執行機制我們可以得出:React
自己實現了一套事件機制,自己模擬了事件冒泡和捕獲的過程,採用了事件代理,批量更新等方法,並且抹平了各個瀏覽器的相容性問題。
React
事件和原生事件的執行順序
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');
})
}
childClick = (e) => {
console.log('react child');
}
parentClick = (e) => {
console.log('react parent');
}
render() {
return (
<div onClick={this.parentClick} ref={ref => this.parent = ref}>
<div onClick={this.childClick} ref={ref => this.child = ref}>
test
</div>
</div>)
}
複製程式碼
執行結果:
由上面的流程我們可以理解:
react
的所有事件都掛載在document
中- 當真實dom觸發後冒泡到
document
後才會對react
事件進行處理 - 所以原生的事件會先執行
- 然後執行
react
合成事件 - 最後執行真正在
document
上掛載的事件
react事件和原生事件可以混用嗎?
react
事件和原生事件最好不要混用。
原生事件中如果執行了stopPropagation
方法,則會導致其他react
事件失效。因為所有元素的事件將無法冒泡到document
上。
由上面的執行機制不難得出,所有的react事件都將無法被註冊。
合成事件、瀏覽器相容
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
複製程式碼
這裡,
e
是一個合成的事件。React
根據 W3C 規範 定義了這個合成事件,所以你不需要擔心跨瀏覽器的相容性問題。
事件處理程式將傳遞 SyntheticEvent
的例項,這是一個跨瀏覽器原生事件包裝器。 它具有與瀏覽器原生事件相同的介面,包括stopPropagation()
和 preventDefault()
,在所有瀏覽器中他們工作方式都相同。
每個SyntheticEvent
物件都具有以下屬性:
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type
複製程式碼
React
合成的SyntheticEvent
採用了事件池,這樣做可以大大節省記憶體,而不會頻繁的建立和銷燬事件物件。
另外,不管在什麼瀏覽器環境下,瀏覽器會將該事件型別統一建立為合成事件,從而達到了瀏覽器相容的目的。