【React】786- 探索 React 合成事件

React 是一個 Facebook 開源的,用于構(gòu)建用戶界面的 JavaScript 庫。
React 目的在于解決:構(gòu)建隨著時間數(shù)據(jù)不斷變化的大規(guī)模應(yīng)用程序。其中 React 合成事件是較為重要的知識點,閱讀完本文,你將收獲:
合成事件的概念和作用; 合成事件與原生事件的 3 個區(qū)別; 合成事件與原生事件的執(zhí)行順序; 合成事件的事件池; 合成事件 4 個常見問題。
接下來和我一起開始學習吧~
一、概念介紹
React 合成事件(SyntheticEvent)是 React 模擬原生 DOM 事件所有能力的一個事件對象,即瀏覽器原生事件的跨瀏覽器包裝器。它根據(jù)?W3C 規(guī)范 來定義合成事件,兼容所有瀏覽器,擁有與瀏覽器原生事件相同的接口。看個簡單示例:
const?button?=?<button?onClick={handleClick}>Leo?按鈕button>
在 React 中,所有事件都是合成的,不是原生 DOM 事件,但可以通過 e.nativeEvent?屬性獲取 DOM 事件。
const?handleClick?=?(e)?=>?console.log(e.nativeEvent);;
const?button?=?<button?onClick={handleClick}>Leo?按鈕button>
學習一個新知識的時候,一定要知道為什么會出現(xiàn)這個技術(shù)。那么 React 為什么使用合成事件?其主要有三個目的:
進行瀏覽器兼容,實現(xiàn)更好的跨平臺
React 采用的是頂層事件代理機制,能夠保證冒泡一致性,可以跨瀏覽器執(zhí)行。React 提供的合成事件用來抹平不同瀏覽器事件對象之間的差異,將不同平臺事件模擬合成事件。
避免垃圾回收
事件對象可能會被頻繁創(chuàng)建和回收,因此 React 引入事件池,在事件池中獲取或釋放事件對象。即 React 事件對象不會被釋放掉,而是存放進一個數(shù)組中,當事件觸發(fā),就從這個數(shù)組中彈出,避免頻繁地去創(chuàng)建和銷毀(垃圾回收)。
方便事件統(tǒng)一管理和事務(wù)機制
本文不介紹源碼啦,對具體實現(xiàn)的源碼有興趣的朋友可以查閱:《React SyntheticEvent》 。
二、原生事件回顧
在開始介紹 React 合成事件之前,我們先簡單回顧 JavaScript 原生事件中幾個重要知識點:
1. 事件捕獲
當某個元素觸發(fā)某個事件(如 onclick ),頂層對象 document 就會發(fā)出一個事件流,隨著 DOM 樹的節(jié)點向目標元素節(jié)點流去,直到到達事件真正發(fā)生的目標元素。在這個過程中,事件相應(yīng)的監(jiān)聽函數(shù)是不會被觸發(fā)的。
2. 事件目標
當?shù)竭_目標元素之后,執(zhí)行目標元素該事件相應(yīng)的處理函數(shù)。如果沒有綁定監(jiān)聽函數(shù),那就不執(zhí)行。
3. 事件冒泡
從目標元素開始,往頂層元素傳播。途中如果有節(jié)點綁定了相應(yīng)的事件處理函數(shù),這些函數(shù)都會被觸發(fā)一次。如果想阻止事件起泡,可以使用 e.stopPropagation() 或者e.cancelBubble=true(IE)來阻止事件的冒泡傳播。
4. 事件委托/事件代理
簡單理解就是將一個響應(yīng)事件委托到另一個元素。當子節(jié)點被點擊時,click 事件向上冒泡,父節(jié)點捕獲到事件后,我們判斷是否為所需的節(jié)點,然后進行處理。其優(yōu)點在于減少內(nèi)存消耗和動態(tài)綁定事件。
二、合成事件與原生事件區(qū)別
React 事件與原生事件很相似,但不完全相同。這里列舉幾個常見區(qū)別:
1. 事件名稱命名方式不同
原生事件命名為純小寫(onclick, onblur),而 React 事件命名采用小駝峰式(camelCase),如 onClick 等:
//?原生事件綁定方式
2. 事件處理函數(shù)寫法不同
原生事件中事件處理函數(shù)為字符串,在 React JSX 語法中,傳入一個函數(shù)作為事件處理函數(shù)。
//?原生事件?事件處理函數(shù)寫法
"handleClick()">Leo?按鈕命名</button>
??????
//?React?合成事件?事件處理函數(shù)寫法
const?button?=?Leo?按鈕命名 button>
3. 阻止默認行為方式不同
在原生事件中,可以通過返回 false?方式來阻止默認行為,但是在 React 中,需要顯式使用 preventDefault()?方法來阻止。這里以阻止 ?標簽?zāi)J打開新頁面為例,介紹兩種事件區(qū)別:
//?原生事件阻止默認行為方式
"https://www.pingan8787.com"?
??onclick="console.log('Leo?阻止原生事件~');?return?false"
>
??Leo?阻止原生事件
</a>
//?React?事件阻止默認行為方式
const?handleClick?=?e?=>?{
??e.preventDefault();
??console.log('Leo?阻止原生事件~');
}
const?clickElement?=?/www.pingan8787.com"?onClick={handleClick}>
??Leo?阻止原生事件
a>
4. 小結(jié)
小結(jié)前面幾點區(qū)別:
| 原生事件 | React 事件 | |
|---|---|---|
| 事件名稱命名方式 | 名稱全部小寫 (onclick, onblur) | 名稱采用小駝峰 (onClick, onBlur) |
| 事件處理函數(shù)語法 | 字符串 | 函數(shù) |
| 阻止默認行為方式 | 事件返回?false | 使用?e.preventDefault()?方法 |

三、React 事件與原生事件執(zhí)行順序
在 React 中,“合成事件”會以事件委托(Event Delegation)方式綁定在組件最上層,并在組件卸載(unmount)階段自動銷毀綁定的事件。這里我們手寫一個簡單示例來觀察 React 事件和原生事件的執(zhí)行順序:
class?App?extends?React.Component<any,?any>?{
??parentRef:?any;
??childRef:?any;
??constructor(props:?any)?{
????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;
觸發(fā)事件后,可以看到控制臺輸出:
原生事件:子元素 DOM 事件監(jiān)聽!?
原生事件:父元素 DOM 事件監(jiān)聽!?
React 事件:子元素事件監(jiān)聽!?
React 事件:父元素事件監(jiān)聽!?
原生事件:document?DOM 事件監(jiān)聽!?
通過上面流程,我們可以理解:
React 所有事件都掛載在 document對象上;當真實 DOM 元素觸發(fā)事件,會冒泡到 document對象后,再處理 React 事件;所以會先執(zhí)行原生事件,然后處理 React 事件; 最后真正執(zhí)行 document上掛載的事件。

四、合成事件的事件池**
1. 事件池介紹
合成事件對象池,是 React?事件系統(tǒng)提供的一種性能優(yōu)化方式。合成事件對象在事件池統(tǒng)一管理,不同類型的合成事件具有不同的事件池。
當事件池未滿時,React 創(chuàng)建新的事件對象,派發(fā)給組件。 當事件池裝滿時,React 從事件池中復(fù)用事件對象,派發(fā)給組件。
關(guān)于“事件池是如何工作”的問題,可以看看下面圖片:

(圖片來自:ReactDeveloper https://juejin.cn/post/6844903862285893639)
2. 事件池分析(React 16 版本)
React 事件池僅支持在 React 16 及更早版本中,在 React 17 已經(jīng)不使用事件池。下面以 React 16 版本為例:
function?handleChange(e)?{
??console.log("原始數(shù)據(jù):",?e.target)
??setTimeout(()?=>?{
????console.log("定時任務(wù) e.target:",?e.target);?//?null
????console.log("定時任務(wù):e:",?e);?
??},?100);
}
function?App()?{
??return?(
????<div?className="App">
??????<button?onClick={handleChange}>測試事件池button>
????div>
??);
}
export?default?App;
可以看到輸出:
在 React 16 及之前的版本,合成事件對象的事件處理函數(shù)全部被調(diào)用之后,所有屬性都會被置為 null?。這時,如果我們需要在事件處理函數(shù)運行之后獲取事件對象的屬性,可以使用 React 提供的 e.persist()?方法,保留所有屬性:
//?只修改?handleChange?方法,其他不變
function?handleChange(e)?{
??//?只增加?persist()?執(zhí)行
??e.persist();
??
??console.log("原始數(shù)據(jù):",?e.target)
??setTimeout(()?=>?{
????console.log("定時任務(wù) e.target:",?e.target);?//?null
????console.log("定時任務(wù):e:",?e);?
??},?100);
}
再看下結(jié)果:

3. 事件池分析(React 17 版本)
由于 Web 端的 React 17 不使用事件池,所有不會存在上述“所有屬性都會被置為 null”的問題。
五、常見問題
1. React 事件中 this 指向問題
在 React 中,JSX 回調(diào)函數(shù)中的 this 經(jīng)常會出問題,在 Class 中方法不會默認綁定 this,就會出現(xiàn)下面情況, this.funName?值為 undefined?:
class?App?extends?React.Component<any,?any>?{
??childClickFun?=?()?=>?{
????console.log("React?事件");
??};
??clickFun()?{
????console.log("React?this?指向問題",?this.childClickFun);?//?undefined
??}
??render()?{
????return?(
????????<div?onClick={this.clickFun}>React?this?指向問題div>
????);
??}
}
export?default?App;
我們有 2 種方式解決這個問題:
使用 bind?方法綁定this?:
class?App?extends?React.Component<any,?any>?{
??constructor(props:?any)?{
????super(props);
????this.clickFun?=?this.clickFun.bind(this);
??}
??
??//?省略其他代碼
}
export?default?App;
將需要使用 this的方法改寫為使用箭頭函數(shù)定義:
class?App?extends?React.Component<any,?any>?{
??clickFun?=?()?=>?{
????console.log("React?this?指向問題",?this.childClickFun);?//?undefined
??}
??
??//?省略其他代碼
}
export?default?App;
或者在回調(diào)函數(shù)中使用箭頭函數(shù):
class?App?extends?React.Component<any,?any>?{
??//?省略其他代碼
??clickFun()?{
????console.log("React?this?指向問題",?this.childClickFun);?//?undefined
??}
??render()?{
????return?(
????????<div?onClick={()?=>?this.clickFun()}>React?this?指向問題div>
????);
??}
}
export?default?App;
2. 向事件傳遞參數(shù)問題
經(jīng)常在遍歷列表時,需要向事件傳遞額外參數(shù),如 id?等,來指定需要操作的數(shù)據(jù),在 React 中,可以使用 2 種方式向事件傳參:
const?List?=?[1,2,3,4];
class?App?extends?React.Component<any,?any>?{
??//?省略其他代碼
??clickFun?(id)?{console.log('當前點擊:',?id)}
??render()?{
????return?(
????????<div>
?????????<h1>第一種:通過 bind 綁定 this 傳參h1>
?????????{
???????????List.map(item?=>?<div?onClick={this.clickFun.bind(this,?item)}>按鈕:{item}div>)
??????????}
?????????<h1>第二種:通過箭頭函數(shù)綁定 this 傳參h1>
?????????{
???????????List.map(item?=>?<div?onClick={()?=>?this.clickFun(item)}>按鈕:{item}div>)
??????????}
????????div>
????);
??}
}
export?default?App;
這兩種方式是等價的:
第一種通過 Function.prototype.bind?實現(xiàn);第二種通過箭頭函數(shù)實現(xiàn)。
3. 合成事件阻止冒泡
官網(wǎng)文檔描述了:
從 v0.14 開始,事件處理器返回 false 時,不再阻止事件傳遞。你可以酌情手動調(diào)用 e.stopPropagation() 或 e.preventDefault() 作為替代方案。
也就是說,在 React 合成事件中,需要阻止冒泡時,可以使用 e.stopPropagation() 或 e.preventDefault() ?方法來解決,另外還可以使用 e.nativeEvent.stopImmediatePropagation() 方法解決。
3.1 e.stopPropagation
對于開發(fā)者來說,更希望使用 e.stopPropagation() 方法來阻止當前 DOM 事件冒泡,但事實上,從前兩節(jié)介紹的執(zhí)行順序可知,e.stopPropagation() 只能阻止合成事件間冒泡,即下層的合成事件,不會冒泡到上層的合成事件。事件本身還都是在 document 上執(zhí)行。所以最多只能阻止 document 事件不能再冒泡到 window 上。
class?App?extends?React.Component<any,?any>?{
??parentRef:?any;
??childRef:?any;
??constructor(props:?any)?{
????super(props);
????this.parentRef?=?React.createRef();
??}
??componentDidMount()?{
????this.parentRef.current?.addEventListener("click",?()?=>?{
??????console.log("阻止原生事件冒泡~");
????});
????document.addEventListener("click",?(e)?=>?{
??????console.log("原生事件:document DOM 事件監(jiān)聽!");
????});
??}
??parentClickFun?=?(e:?any)?=>?{
????e.stopPropagation();
????console.log("阻止合成事件冒泡~");
??};
??render()?{
????return?(
??????this.parentRef}?onClick={this.parentClickFun}>
????????點擊測試“合成事件和原生事件是否可以混用”
??????</div>
????);
??}
}
export?default?App;
輸出結(jié)果:
阻止原生事件冒泡~?
阻止合成事件冒泡~?
3.2 e.nativeEvent.stopImmediatePropagation
該方法可以阻止監(jiān)聽同一事件的其他事件監(jiān)聽器被調(diào)用。在 React 中,一個組件只能綁定一個同類型的事件監(jiān)聽器,當重復(fù)定義時,后面的監(jiān)聽器會覆蓋之前的。事實上 nativeEvent 的 stopImmediatePropagation只能阻止綁定在 document 上的事件監(jiān)聽器。而合成事件上的 e.nativeEvent.stopImmediatePropagation() ?能阻止合成事件不會冒泡到 document 上。
舉一個實際案例:實現(xiàn)點擊空白處關(guān)閉菜單的功能:當菜單打開時,在 document 上動態(tài)注冊事件,用來關(guān)閉菜單。
點擊菜單內(nèi)部,由于不冒泡,會正常執(zhí)行菜單點擊。 點擊菜單外部,執(zhí)行document上事件,關(guān)閉菜單。
在菜單關(guān)閉的一刻,在 document 上移除該事件,這樣就不會重復(fù)執(zhí)行該事件,浪費性能,也可以在 window 上注冊事件,這樣可以避開 document。**
4. 合成事件和原生事件是否可以混用
合成事件和原生事件最好不要混用。原生事件中如果執(zhí)行了stopPropagation方法,則會導(dǎo)致其他React事件失效。因為所有元素的事件將無法冒泡到document上。通過前面介紹的兩者事件執(zhí)行順序來看,所有的 React 事件都將無法被注冊。通過代碼一起看看:
class?App?extends?React.Component<any,?any>?{
??parentRef:?any;
??childRef:?any;
??constructor(props:?any)?{
????super(props);
????this.parentRef?=?React.createRef();
??}
??componentDidMount()?{
????this.parentRef.current?.addEventListener("click",?(e:?any)?=>?{
?????e.stopPropagation();
??????console.log("阻止原生事件冒泡~");
????});
????document.addEventListener("click",?(e)?=>?{
??????console.log("原生事件:document DOM 事件監(jiān)聽!");
????});
??}
??parentClickFun?=?(e:?any)?=>?{
????console.log("阻止合成事件冒泡~");
??};
??render()?{
????return?(
??????this.parentRef}?onClick={this.parentClickFun}>
????????點擊測試“合成事件和原生事件是否可以混用”
??????</div>
????);
??}
}
export?default?App;
輸出結(jié)果:
阻止原生事件冒泡~?
好了,本文就寫到這里,建議大家可以再回去看下官方文檔《合成事件》《事件處理》章節(jié)理解,有興趣的朋友也可以閱讀源碼《React SyntheticEvent.js》。
總結(jié)
最后在回顧下本文學習目標:
合成事件的概念和作用; 合成事件與原生事件的 3 個區(qū)別; 合成事件與原生事件的執(zhí)行順序; 合成事件的事件池; 合成事件 4 個常見問題。
你是否都清楚了?歡迎一起討論學習。
參考文章
1.《事件處理與合成事件(react)》
2.官方文檔《合成事件》《事件處理》
3.《React合成事件和DOM原生事件混用須知》
4.《React 合成事件系統(tǒng)之事件池》

1. JavaScript 重溫系列(22篇全)
2. ECMAScript 重溫系列(10篇全)
3. JavaScript設(shè)計模式 重溫系列(9篇全) 4.?正則 / 框架 / 算法等 重溫系列(16篇全) 5.?Webpack4 入門(上)||?Webpack4 入門(下) 6.?MobX 入門(上)?||??MobX 入門(下) 7. 80+篇原創(chuàng)系列匯總 回復(fù)“加群”與大佬們一起交流學習~
點擊“閱讀原文”查看 80+ 篇原創(chuàng)文章

