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

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

三、React 事件與原生事件執(zhí)行順序
在 React 中,“合成事件”會(huì)以事件委托(Event Delegation)方式綁定在組件最上層,并在組件卸載(unmount)階段自動(dòng)銷(xiāo)毀綁定的事件。這里我們手寫(xiě)一個(gè)簡(jiǎn)單示例來(lái)觀察 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)聽(tīng)!");
????});
????this.childRef.current?.addEventListener("click",?()?=>?{
??????console.log("原生事件:子元素 DOM 事件監(jiān)聽(tīng)!");
????});
????document.addEventListener("click",?(e)?=>?{
??????console.log("原生事件:document DOM 事件監(jiān)聽(tīng)!");
????});
??}
??parentClickFun?=?()?=>?{
????console.log("React 事件:父元素事件監(jiān)聽(tīng)!");
??};
??childClickFun?=?()?=>?{
????console.log("React 事件:子元素事件監(jiān)聽(tīng)!");
??};
??render()?{
????return?(
??????<div?ref={this.parentRef}?onClick={this.parentClickFun}>
????????<div?ref={this.childRef}?onClick={this.childClickFun}>
??????????分析事件執(zhí)行順序
????????div>
??????div>
????);
??}
}
export?default?App;
觸發(fā)事件后,可以看到控制臺(tái)輸出:
原生事件:子元素 DOM 事件監(jiān)聽(tīng)!?
原生事件:父元素 DOM 事件監(jiān)聽(tīng)!?
React 事件:子元素事件監(jiān)聽(tīng)!?
React 事件:父元素事件監(jiān)聽(tīng)!?
原生事件:document?DOM 事件監(jiān)聽(tīng)!?
通過(guò)上面流程,我們可以理解:
React 所有事件都掛載在 document對(duì)象上;當(dāng)真實(shí) DOM 元素觸發(fā)事件,會(huì)冒泡到 document對(duì)象后,再處理 React 事件;所以會(huì)先執(zhí)行原生事件,然后處理 React 事件; 最后真正執(zhí)行 document上掛載的事件。

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

(圖片來(lái)自: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("定時(shí)任務(wù) e.target:",?e.target);?//?null
????console.log("定時(shí)任務(wù):e:",?e);?
??},?100);
}
function?App()?{
??return?(
????<div?className="App">
??????<button?onClick={handleChange}>測(cè)試事件池button>
????div>
??);
}
export?default?App;
可以看到輸出:
在 React 16 及之前的版本,合成事件對(duì)象的事件處理函數(shù)全部被調(diào)用之后,所有屬性都會(huì)被置為 null?。這時(shí),如果我們需要在事件處理函數(shù)運(yùn)行之后獲取事件對(duì)象的屬性,可以使用 React 提供的 e.persist()?方法,保留所有屬性:
//?只修改?handleChange?方法,其他不變
function?handleChange(e)?{
??//?只增加?persist()?執(zhí)行
??e.persist();
??
??console.log("原始數(shù)據(jù):",?e.target)
??setTimeout(()?=>?{
????console.log("定時(shí)任務(wù) e.target:",?e.target);?//?null
????console.log("定時(shí)任務(wù):e:",?e);?
??},?100);
}
再看下結(jié)果:

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

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

