React 合成事件的實(shí)現(xiàn)
?? 什么是合成事件?
合成事件(SyntheticEvent) 是React模擬原生DOM事件所有能力的一個(gè)事件對(duì)象,即瀏覽器原生事件的跨瀏覽器包裝器。它根據(jù)W3C規(guī)范定義,兼容所有瀏覽器,擁有與瀏覽器原生事件相同的接口。
// React中的事件使用
function Button() {
const handleClick = (e) => {
console.log(e) // 這是合成事件對(duì)象,不是原生事件
console.log(e.nativeEvent) // 通過nativeEvent獲取原生事件
}
return <button onClick={handleClick}>點(diǎn)擊我</button>
}
??為什么需要合成事件?
React設(shè)計(jì)合成事件主要有三個(gè)目的:
- 跨瀏覽器兼容:抹平不同瀏覽器事件對(duì)象的差異,提供一致的API
- 性能優(yōu)化:通過事件委托機(jī)制,減少內(nèi)存消耗
- 統(tǒng)一管理:方便事件的事務(wù)機(jī)制和優(yōu)先級(jí)調(diào)度
研究表明,在大型列表中,事件委托可以減少90%以上的事件綁定,顯著提升性能。
??? 合成事件的核心原理
1?? 事件委托
React并不是將事件綁定到具體的DOM元素上,而是在頂層統(tǒng)一監(jiān)聽。
版本差異:
- React 16及之前:事件綁定在
document上 - React 17+:事件綁定在
root容器上(id="root"的DOM元素)
// React 17+ 的事件綁定位置
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
// 所有事件都委托在root元素上
為什么改到root? 這有利于多個(gè)React版本共存,避免微前端等場(chǎng)景的沖突。
2?? 事件注冊(cè)流程
React事件系統(tǒng)的核心架構(gòu)分為三個(gè)層次:
// 簡化版的事件注冊(cè)機(jī)制
// 1. 事件注冊(cè):registerEvents
// 2. 事件監(jiān)聽:listenToAllSupportedEvents
// 3. 事件合成:SyntheticBaseEvent
// 4. 事件派發(fā):dispatchEvent
事件注冊(cè)源碼簡化版:
// 注冊(cè)不同類型的事件 registerSimpleEvents(); // 注冊(cè)click、keyup等基礎(chǔ)事件 registerEvents$2(); // 注冊(cè)onMouseEnter等單階段事件 registerEvents$1(); // 注冊(cè)onChange相關(guān)事件 registerEvents$3(); // 注冊(cè)onSelect相關(guān)事件 registerEvents(); // 注冊(cè)onBeforeInput等事件
3?? 事件存儲(chǔ)與分發(fā)
React內(nèi)部維護(hù)了一個(gè)事件插件系統(tǒng),采用模塊化設(shè)計(jì),每個(gè)插件負(fù)責(zé)特定類型的事件處理。
// 簡化版的事件分發(fā)邏輯
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
// 找到觸發(fā)事件的DOM元素對(duì)應(yīng)的fiber節(jié)點(diǎn)
const target = nativeEvent.target
const targetInst = getClosestInstanceFromNode(target)
// 創(chuàng)建合成事件
const events = extractEvents(
domEventName,
targetInst,
nativeEvent,
target
)
// 按階段分發(fā)事件
events.forEach(event => {
runEventsInBatch(event)
})
}
?? 合成事件 vs 原生事件
核心區(qū)別對(duì)比表
| 對(duì)比維度 | 原生事件 | React合成事件 |
|---|---|---|
| 事件名稱 | 純小寫(onclick, onblur) | 小駝峰(onClick, onBlur) |
| 處理函數(shù) | 字符串 | 函數(shù) |
| 阻止默認(rèn)行為 | 返回false | 必須顯式調(diào)用preventDefault() |
| 綁定方式 | addEventListener | JSX屬性 |
| 內(nèi)存消耗 | 每個(gè)元素獨(dú)立綁定 | 事件委托,統(tǒng)一管理 |
| 執(zhí)行順序 | 直接在目標(biāo)元素觸發(fā) | 冒泡到頂層后統(tǒng)一處理 |
執(zhí)行順序演示
class EventOrderDemo extends React.Component {
componentDidMount() {
// 原生事件監(jiān)聽
this.refs.button.addEventListener('click', () => {
console.log('1. 原生事件:子元素')
})
document.addEventListener('click', () => {
console.log('4. 原生事件:document')
})
}
handleParentClick = () => {
console.log('3. React事件:父元素')
}
handleChildClick = () => {
console.log('2. React事件:子元素')
}
render() {
return (
<div onClick={this.handleParentClick} ref="parent">
<button onClick={this.handleChildClick} ref="button">
點(diǎn)擊我
</button>
</div>
)
}
}
// 輸出順序:
// 1. 原生事件:子元素
// 2. React事件:子元素
// 3. React事件:父元素
// 4. 原生事件:document
關(guān)鍵結(jié)論:原生事件先執(zhí)行,然后執(zhí)行React事件,最后執(zhí)行document上的原生事件。
??♂? 事件池機(jī)制(??????)
React 16及之前的事件池
在React 16及更早版本中,React使用事件池來管理合成事件對(duì)象。
// React 16 示例
function handleClick(e) {
console.log(e.target) // 正常輸出
setTimeout(() => {
console.log(e.target) // ? null!事件對(duì)象已被回收
}, 100)
}
// 解決方案:使用e.persist()
function handleClickCorrect(e) {
e.persist() // 從事件池中移除,保留屬性
setTimeout(() => {
console.log(e.target) // ? 正常輸出
}, 100)
}
事件池的工作原理:
- 事件對(duì)象會(huì)被重用,避免頻繁創(chuàng)建銷毀
- 事件處理函數(shù)執(zhí)行完后,所有屬性會(huì)被置為
null - 默認(rèn)池大小為10個(gè)對(duì)象
React 17+ 的變更
重要:React 17 開始,Web端不再使用事件池!
// React 17+,不需要e.persist()
function handleClick(e) {
setTimeout(() => {
console.log(e.target) // ? 正常輸出,事件池已移除
}, 100)
}
官方解釋:現(xiàn)代瀏覽器性能已經(jīng)足夠好,事件池優(yōu)化帶來的收益不及復(fù)雜性成本。
?? 合成事件對(duì)象屬性
合成事件對(duì)象提供了豐富的屬性和方法:
function EventPropertiesDemo() {
const handleEvent = (e) => {
// 基礎(chǔ)屬性
console.log(e.type) // 事件類型:click
console.log(e.target) // 觸發(fā)事件的DOM元素
console.log(e.currentTarget) // 當(dāng)前處理事件的DOM元素
console.log(e.nativeEvent) // 原生事件對(duì)象
// 事件方法
e.preventDefault() // 阻止默認(rèn)行為
e.stopPropagation() // 阻止冒泡
// 狀態(tài)查詢
console.log(e.isDefaultPrevented()) // 是否已阻止默認(rèn)行為
console.log(e.isPropagationStopped()) // 是否已阻止冒泡
// 其他屬性
console.log(e.bubbles) // 是否可冒泡
console.log(e.cancelable) // 是否可取消
console.log(e.timeStamp) // 事件觸發(fā)時(shí)間戳
}
return <button onClick={handleEvent}>測(cè)試事件</button>
}
? 性能優(yōu)化最佳實(shí)踐
1?? 使用事件委托
// ? 不推薦:為每個(gè)列表項(xiàng)綁定事件
function BadList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleItem(item)}>
{item.name}
</li>
))}
</ul>
)
}
// ? 推薦:使用事件委托
function GoodList({ items }) {
const handleListClick = (e) => {
const target = e.target
if (target.tagName === 'LI') {
const id = target.dataset.id
console.log('點(diǎn)擊了項(xiàng)目:', id)
}
}
return (
<ul onClick={handleListClick}>
{items.map(item => (
<li key={item.id} data-id={item.id}>
{item.name}
</li>
))}
</ul>
)
}
2?? 避免混用原生事件和合成事件
// ? 危險(xiǎn):混用可能導(dǎo)致事件不執(zhí)行
function BadMixing() {
useEffect(() => {
document.addEventListener('click', (e) => {
e.stopPropagation() // 阻止了冒泡,React事件可能收不到
})
}, [])
return <button onClick={() => console.log('不會(huì)執(zhí)行')}>點(diǎn)擊</button>
}
// ? 建議:統(tǒng)一使用React事件
function GoodPractice() {
return <button onClick={() => console.log('正常執(zhí)行')}>點(diǎn)擊</button>
}
3?? 合理使用preventDefault和stopPropagation
function FormDemo() {
const handleSubmit = (e) => {
// ? 阻止表單提交的默認(rèn)行為
e.preventDefault()
// 處理表單邏輯
submitForm()
}
const handleButtonClick = (e) => {
// 只在必要時(shí)阻止冒泡
if (shouldStopPropagation) {
e.stopPropagation()
}
}
return (
<form onSubmit={handleSubmit}>
<button onClick={handleButtonClick}>提交</button>
</form>
)
}
?? 難點(diǎn)解析
Q1:React合成事件和原生事件的區(qū)別?
滿分回答思路:
- 定義區(qū)別:合成事件是React的跨瀏覽器包裝器,原生事件是瀏覽器原生實(shí)現(xiàn)
- 命名方式:合成事件小駝峰(onClick),原生事件全小寫(onclick)
- 處理函數(shù):合成事件傳函數(shù),原生事件傳字符串
- 阻止默認(rèn):合成事件必須用preventDefault(),原生可return false
- 綁定機(jī)制:合成事件用事件委托統(tǒng)一管理,原生事件直接綁定
- 內(nèi)存優(yōu)化:合成事件減少內(nèi)存消耗,原生事件綁定越多內(nèi)存消耗越大
Q2:合成事件的執(zhí)行順序是怎樣的?
觸發(fā)事件 → 原生事件(目標(biāo)元素)→ React事件(冒泡階段)→ document事件
關(guān)鍵點(diǎn):原生事件先執(zhí)行,如果原生事件阻止冒泡,React事件可能不會(huì)執(zhí)行(阻止合成事件不會(huì)影響原生事件)。
Q3:React 17對(duì)事件系統(tǒng)做了哪些改進(jìn)?
- 事件綁定位置:從document改為root容器
- 移除事件池:不再需要e.persist()
- onScroll冒泡:不再冒泡,匹配瀏覽器行為
- 優(yōu)化微前端:多個(gè)React版本可共存
Q4:如何在React事件中獲取異步訪問事件對(duì)象?
// React 16及以前:需要用e.persist()
function handleAsync(e) {
e.persist()
setTimeout(() => {
console.log(e.target)
}, 100)
}
// React 17+:直接使用即可
function handleAsync(e) {
setTimeout(() => {
console.log(e.target) // 沒問題
}, 100)
}
?? 總結(jié):合成事件的核心價(jià)值
| 維度 | 價(jià)值體現(xiàn) |
|---|---|
| 兼容性 | 抹平瀏覽器差異,提供一致API |
| 性能 | 事件委托減少90%+事件綁定 |
| 內(nèi)存 | 事件池機(jī)制(16及以前)減少GC壓力 |
| 可維護(hù)性 | 統(tǒng)一管理,自動(dòng)清理,避免內(nèi)存泄漏 |
| 開發(fā)體驗(yàn) | 聲明式API,符合W3C規(guī)范,上手簡單 |
一句話總結(jié):
React合成事件是一套基于事件委托、跨瀏覽器兼容、性能優(yōu)化的事件系統(tǒng),它通過頂層監(jiān)聽和統(tǒng)一分發(fā),為開發(fā)者提供了穩(wěn)定高效的事件處理機(jī)制。
到此這篇關(guān)于React 合成事件的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)React 合成事件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React實(shí)現(xiàn)歌詞滾動(dòng)效果(跟隨音樂播放時(shí)間滾動(dòng))
這篇文章主要為大家詳細(xì)介紹了React實(shí)現(xiàn)歌詞滾動(dòng)效果(跟隨音樂播放使勁按滾動(dòng)),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2024-02-02
解決react-connect中使用forwardRef遇到的問題
這篇文章主要介紹了解決react-connect中使用forwardRef遇到的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
使用React hook實(shí)現(xiàn)remember me功能
相信大家在使用 React 寫頁面的時(shí)候都遇到過完成 Remember me 的需求吧!本文就將這個(gè)需求封裝在一個(gè) React hook 中以供后續(xù)的使用,覺得有用的同學(xué)可以收藏起來以備不時(shí)之需,感興趣的小伙伴跟著小編一起來看看吧2024-04-04
全棧輕量級(jí)搭配之Remix Prisma Sqlite使用分析
這篇文章主要為大家介紹了全棧輕量級(jí)搭配之Remix Prisma Sqlite使用示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
React循環(huán)遍歷渲染數(shù)組和對(duì)象元素方式
這篇文章主要介紹了React循環(huán)遍歷渲染數(shù)組和對(duì)象元素方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09

