面試官:說說對React事件機制的理解?
本文轉(zhuǎn)載自微信公眾號「JS每日一題」,作者灰灰。轉(zhuǎn)載本文請聯(lián)系JS每日一題公眾號。
一、是什么
React基于瀏覽器的事件機制自身實現(xiàn)了一套事件機制,包括事件注冊、事件的合成、事件冒泡、事件派發(fā)等
在React中這套事件機制被稱之為合成事件
合成事件(SyntheticEvent)
合成事件是 React模擬原生 DOM事件所有能力的一個事件對象,即瀏覽器原生事件的跨瀏覽器包裝器
根據(jù) W3C規(guī)范來定義合成事件,兼容所有瀏覽器,擁有與瀏覽器原生事件相同的接口,例如:
- const button = <button onClick={handleClick}>按鈕</button>
如果想要獲得原生DOM事件,可以通過e.nativeEvent屬性獲取
- const handleClick = (e) => console.log(e.nativeEvent);;
- const button = <button onClick={handleClick}>按鈕</button>
從上面可以看到React事件和原生事件也非常的相似,但也有一定的區(qū)別:
- 事件名稱命名方式不同
- // 原生事件綁定方式
- <button onclick="handleClick()">按鈕命名</button>
- // React 合成事件綁定方式
- const button = <button onClick={handleClick}>按鈕命名</button>
- 事件處理函數(shù)書寫不同
- // 原生事件 事件處理函數(shù)寫法
- <button onclick="handleClick()">按鈕命名</button>
- // React 合成事件 事件處理函數(shù)寫法
- const button = <button onClick={handleClick}>按鈕命名</button>
雖然onclick看似綁定到DOM元素上,但實際并不會把事件代理函數(shù)直接綁定到真實的節(jié)點上,而是把所有的事件綁定到結(jié)構(gòu)的最外層,使用一個統(tǒng)一的事件去監(jiān)聽
這個事件監(jiān)聽器上維持了一個映射來保存所有組件內(nèi)部的事件監(jiān)聽和處理函數(shù)。當組件掛載或卸載時,只是在這個統(tǒng)一的事件監(jiān)聽器上插入或刪除一些對象
當事件發(fā)生時,首先被這個統(tǒng)一的事件監(jiān)聽器處理,然后在映射里找到真正的事件處理函數(shù)并調(diào)用。這樣做簡化了事件處理和回收機制,效率也有很大提升
二、執(zhí)行順序
關于React合成事件與原生事件執(zhí)行順序,可以看看下面一個例子:
- import React from 'react';
- class App extends React.Component{
- constructor(props) {
- super(props);
- this.parentRef = React.createRef();
- this.childRef = React.createRef();
- }
- componentDidMount() {
- console.log("React componentDidMount!");
- this.parentRef.current?.addEventListener("click", () => {
- console.log("原生事件:父元素 DOM 事件監(jiān)聽!");
- });
- this.childRef.current?.addEventListener("click", () => {
- console.log("原生事件:子元素 DOM 事件監(jiān)聽!");
- });
- document.addEventListener("click", (e) => {
- console.log("原生事件:document DOM 事件監(jiān)聽!");
- });
- }
- parentClickFun = () => {
- console.log("React 事件:父元素事件監(jiān)聽!");
- };
- childClickFun = () => {
- console.log("React 事件:子元素事件監(jiān)聽!");
- };
- render() {
- return (
- <div ref={this.parentRef} onClick={this.parentClickFun}>
- <div ref={this.childRef} onClick={this.childClickFun}>
- 分析事件執(zhí)行順序
- </div>
- </div>
- );
- }
- }
- export default App;
輸出順序為:
- 原生事件:子元素 DOM 事件監(jiān)聽!
- 原生事件:父元素 DOM 事件監(jiān)聽!
- React 事件:子元素事件監(jiān)聽!
- React 事件:父元素事件監(jiān)聽!
- 原生事件:document DOM 事件監(jiān)聽!
可以得出以下結(jié)論:
- React 所有事件都掛載在 document 對象上
- 當真實 DOM 元素觸發(fā)事件,會冒泡到 document 對象后,再處理 React 事件
- 所以會先執(zhí)行原生事件,然后處理 React 事件
- 最后真正執(zhí)行 document 上掛載的事件
對應過程如圖所示:
所以想要阻止不同時間段的冒泡行為,對應使用不同的方法,對應如下:
- 阻止合成事件間的冒泡,用e.stopPropagation()
- 阻止合成事件與最外層 document 上的事件間的冒泡,用e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件與最外層document上的原生事件上的冒泡,通過判斷e.target來避免
- document.body.addEventListener('click', e => {
- if (e.target && e.target.matches('div.code')) {
- return;
- }
- this.setState({ active: false, }); });
- }
三、總結(jié)
React事件機制總結(jié)如下:
- React 上注冊的事件最終會綁定在document這個 DOM 上,而不是 React 組件對應的 DOM(減少內(nèi)存開銷就是因為所有的事件都綁定在 document 上,其他節(jié)點沒有綁定事件)
- React 自身實現(xiàn)了一套事件冒泡機制,所以這也就是為什么我們 event.stopPropagation()無效的原因。
- React 通過隊列的形式,從觸發(fā)的組件向父組件回溯,然后調(diào)用他們 JSX 中定義的 callback
- React 有一套自己的合成事件 SyntheticEvent
參考文獻
- https://zh-hans.reactjs.org/docs/events.html
- https://segmentfault.com/a/1190000015725214?utm_source=sf-similar-article
- https://segmentfault.com/a/1190000038251163