關於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
採用了事件池,這樣做可以大大節省內存,而不會頻繁的創建和銷燬事件對象。
另外,不管在什麼瀏覽器環境下,瀏覽器會將該事件類型統一創建爲合成事件,從而達到了瀏覽器兼容的目的。