雅封裝 addEventListener 的 React Hook 實(shí)踐指南)
前端【免費(fèi)下載鏈接】hooksA high-quality reliable React Hooks library. https://alibaba.github.io/hooks/項(xiàng)目地址https://gitcode.com/gh_mirrors/hooks/hooks點(diǎn)擊查看免費(fèi)下載導(dǎo)讀useEventListener是 ahooks當(dāng)前倉(cāng)庫(kù)位于 packages/hooks/src/useEventListener中用于在 React 函數(shù)組件里「優(yōu)雅使用 addEventListener」的核心 Hook。它把事件監(jiān)聽的注冊(cè)、清理、依賴更新、target解析等繁瑣工作全部封裝起來(lái)讓開發(fā)者可以用聲明式的方式監(jiān)聽window、Document、任意 DOM 元素乃至多個(gè)事件并在組件卸載時(shí)自動(dòng)移除監(jiān)聽、避免內(nèi)存泄漏。讀完本文你將掌握useEventListener的三種典型用法基礎(chǔ)綁定、監(jiān)聽鍵盤事件、同時(shí)監(jiān)聽多個(gè)事件、全部參數(shù)與默認(rèn)值以及它背后的useLatestuseEffectWithTarget源碼實(shí)現(xiàn)原理與測(cè)試驗(yàn)證方式。為什么需要 useEventListener在 React 函數(shù)組件中直接使用原生addEventListener需要開發(fā)者自己處理三件麻煩事清理時(shí)機(jī)組件卸載時(shí)忘記removeEventListener會(huì)造成事件監(jiān)聽泄漏或?qū)σ研遁d的 DOM 節(jié)點(diǎn)觸發(fā)回調(diào)報(bào)錯(cuò)依賴更新事件處理函數(shù)中捕獲的閉包變量過(guò)期后需要反復(fù)解綁再重新綁定代碼冗長(zhǎng)且容易出錯(cuò)目標(biāo)元素解析window、document、普通 DOM 節(jié)點(diǎn)、ref、返回 DOM 的函數(shù)等不同目標(biāo)形態(tài)需要統(tǒng)一解析邏輯。useEventListener通過(guò)一個(gè) Hook 就解決了上述所有問(wèn)題其整體 API 聲明如下見 index.zh-CN.mduseEventListener( eventName: string, handler: (ev: Event) void, options?: Options, );eventName為事件名稱handler為事件處理函數(shù)options為可選配置。值得注意的是在源碼實(shí)現(xiàn)中它還通過(guò) TypeScript 函數(shù)重載對(duì)HTMLElementEventMap、ElementEventMap、DocumentEventMap、WindowEventMap提供了事件名與事件對(duì)象類型的完整推導(dǎo)讓ev參數(shù)具備精準(zhǔn)的類型提示。三種典型用法基礎(chǔ)用法監(jiān)聽 DOM 節(jié)點(diǎn)點(diǎn)擊第一個(gè)官方示例見 demo/demo1.tsx展示了如何監(jiān)聽某個(gè)按鈕的點(diǎn)擊事件import { useState, useRef } from react; import { useEventListener } from ahooks; export default () { const [value, setValue] useState(0); const ref useRef(null); useEventListener( click, () { setValue(value 1); }, { target: ref }, ); return ( button ref{ref} typebutton You click {value} times /button ); };要點(diǎn)說(shuō)明通過(guò){ target: ref }把監(jiān)聽目標(biāo)指向按鈕對(duì)應(yīng)的 ref點(diǎn)擊按鈕時(shí)value自增不需要手動(dòng)清理組件卸載或 target 變化時(shí)Hook 會(huì)自動(dòng)完成解綁如果你不傳target默認(rèn)監(jiān)聽目標(biāo)就是window見下文 Options 表格。監(jiān)聽 keydown 事件默認(rèn)監(jiān)聽 window第二個(gè)示例見 demo/demo2.tsx演示了不指定target、直接監(jiān)聽全局鍵盤事件import { useState } from react; import { useEventListener } from ahooks; export default () { const [value, setValue] useState(); useEventListener(keydown, (ev) { setValue(ev.code); }); return pYour press key is {value}/p; };由于未傳target監(jiān)聽器默認(rèn)掛在window上按下任意按鍵即可在頁(yè)面中看到對(duì)應(yīng)的ev.code如KeyA、Enter等。這在實(shí)現(xiàn)快捷鍵、全局按鍵統(tǒng)計(jì)等場(chǎng)景非常實(shí)用。同時(shí)監(jiān)聽多個(gè)事件第三個(gè)示例見 demo/demo3.tsx展示了eventName支持字符串?dāng)?shù)組一次調(diào)用即可監(jiān)聽多個(gè)事件import { useRef, useState } from react; import { useEventListener } from ahooks; export default () { const ref useRef(null); const [value, setValue] useState(); useEventListener( [mouseenter, mouseleave], (ev) { setValue(ev.type); }, { target: ref }, ); return ( button ref{ref} typebutton You Option is {value} /button ); };鼠標(biāo)移入按鈕時(shí)ev.type為mouseenter移出時(shí)為mouseleave。從源碼實(shí)現(xiàn)可以看到內(nèi)部會(huì)把字符串事件名統(tǒng)一轉(zhuǎn)成數(shù)組再逐個(gè)調(diào)用addEventListener因此單事件與多事件共用同一套注冊(cè)/清理邏輯。參數(shù)詳解Params參數(shù)說(shuō)明類型默認(rèn)值eventName事件名稱string|string[]-handler處理函數(shù)(ev: Event) void-options設(shè)置Options-Options參數(shù)說(shuō)明類型默認(rèn)值targetDOM 節(jié)點(diǎn)或者 ref(() Element)|Element|React.MutableRefObjectElement|Window|Documentwindowcapture可選listener 會(huì)在該類型的事件捕獲階段傳播到該 EventTarget 時(shí)觸發(fā)booleanfalseonce可選listener 在添加之后最多只調(diào)用一次為true時(shí)會(huì)在被調(diào)用后自動(dòng)移除booleanfalsepassive可選為true時(shí)表示 listener 永遠(yuǎn)不會(huì)調(diào)用preventDefault()若仍調(diào)用客戶端會(huì)忽略該調(diào)用并拋出控制臺(tái)警告booleanfalseenable可選是否開啟監(jiān)聽booleantrue這些選項(xiàng)在源碼的類型定義中與文檔完全一致且在注冊(cè)監(jiān)聽時(shí)被原樣透?jìng)鹘o原生addEventListener的options參數(shù)見 index.ts。因此capture、once、passive的行為完全對(duì)齊瀏覽器原生 EventTarget.addEventListener 的對(duì)應(yīng)選項(xiàng)語(yǔ)義enable是 ahooks 額外提供的開關(guān)當(dāng)它為false時(shí)Hook 不會(huì)注冊(cè)任何監(jiān)聽當(dāng)它從false重新變?yōu)閠rue時(shí)會(huì)自動(dòng)重新綁定源碼見 index.ts。這對(duì)于「用戶登錄后才開始監(jiān)聽」「組件掛載但功能尚未就緒」這類條件式監(jiān)聽場(chǎng)景非常有用。target 的四種合法形態(tài)從 domTarget.ts 可以看到BasicTarget的定義target支持四種寫法寫法說(shuō)明示例直接傳 DOM 元素適用于元素已經(jīng)存在的情況{ target: document.querySelector(#btn) }傳 React ref最常見寫法與useRef搭配{ target: ref }傳函數(shù)返回 DOM惰性解析適合元素可能延遲出現(xiàn)的場(chǎng)景{ target: () container }不傳默認(rèn)監(jiān)聽windowuseEventListener(keydown, handler)在測(cè)試用例中可以看到{ target: () container }這種函數(shù)式寫法的實(shí)際應(yīng)用。源碼原理四個(gè)關(guān)鍵設(shè)計(jì)useEventListener的實(shí)現(xiàn)并不復(fù)雜但包含四個(gè)非常值得借鑒的設(shè)計(jì)完整代碼見 index.ts。1. useLatest 保證 handler 永遠(yuǎn)最新const handlerRef useLatest(handler);handler每次渲染都可能是一個(gè)新函數(shù)。如果直接把它綁定到addEventListener那么閉包捕獲的就是舊值。ahooks 用 useLatest 把 handler 存進(jìn) ref保證每次觸發(fā)事件時(shí)執(zhí)行的始終是「最近一次渲染」的 handler從而既不需要因?yàn)?handler 變化而反復(fù)解綁、重綁也不會(huì)讀到過(guò)期閉包。這是useEventListener性能與正確性的關(guān)鍵。2. useEffectWithTarget隨 target 變化的副作用useEffectWithTarget( () { /* 注冊(cè)與清理邏輯 */ }, [eventName, options.capture, options.once, options.passive, enable], options.target, );這里沒有直接使用useEffect而是用了 ahooks 內(nèi)部封裝的 useEffectWithTarget。它的作用是當(dāng) target 對(duì)應(yīng)的實(shí)際 DOM 元素發(fā)生變化時(shí)也能觸發(fā)副作用重建。從底層實(shí)現(xiàn) createEffectWithTarget.ts 可以看到其核心邏輯首次渲染時(shí)直接執(zhí)行 effect之后每次渲染都比較「上一輪的 target 解析結(jié)果」與「當(dāng)前 target 解析結(jié)果」以及依賴數(shù)組是否變化只有真正變化時(shí)才先執(zhí)行上一次的清理函數(shù)、再執(zhí)行新的 effect組件卸載時(shí)通過(guò)useUnmount自動(dòng)執(zhí)行最后一次的清理函數(shù)見 createEffectWithTarget.ts。這意味著當(dāng)ref從空變?yōu)橹赶蛘鎸?shí) DOM 節(jié)點(diǎn)例如條件渲染的元素、或 DOM 節(jié)點(diǎn)被替換時(shí)監(jiān)聽器都能正確地「先解綁、再重綁」。這個(gè)機(jī)制也解釋了為何 Hook 能做到「無(wú)需手動(dòng)清理」。3. getTargetElement統(tǒng)一的 target 解析const targetElement getTargetElement(options.target, window); if (!targetElement?.addEventListener) { return; }getTargetElement 會(huì)依次處理三種 target 形態(tài)若target是函數(shù)則調(diào)用它拿到元素若target含有current屬性即 ref 對(duì)象則取target.current否則把target本身當(dāng)作元素。同時(shí)它還做了兩類重要保護(hù)SSR 安全非瀏覽器環(huán)境!isBrowser直接返回undefined不會(huì)在服務(wù)端渲染時(shí)報(bào)錯(cuò)容錯(cuò)解析結(jié)果若沒有addEventListener方法例如元素尚未掛載直接return跳過(guò)注冊(cè)不會(huì)拋異常。4. 注冊(cè)與清理的對(duì)稱實(shí)現(xiàn)const eventNameArray Array.isArray(eventName) ? eventName : [eventName]; eventNameArray.forEach((event) { targetElement.addEventListener(event, eventListener, { capture: options.capture, once: options.once, passive: options.passive, }); }); return () { eventNameArray.forEach((event) { targetElement.removeEventListener(event, eventListener, { capture: options.capture, }); }); };注意兩個(gè)細(xì)節(jié)注冊(cè)與清理共用同一個(gè)eventListener包裝函數(shù)內(nèi)部調(diào)用handlerRef.current保證removeEventListener能精確移除同一個(gè)監(jiān)聽器引用清理時(shí)只需傳capture因?yàn)閞emoveEventListener的匹配只依賴capture無(wú)需once/passive。整個(gè) effect 返回的清理函數(shù)會(huì)在依賴變化或組件卸載時(shí)自動(dòng)執(zhí)行與原生 API 保持完全對(duì)稱。測(cè)試驗(yàn)證監(jiān)聽生命周期如何被保障倉(cāng)庫(kù)為useEventListener提供了完整的單元測(cè)試見tests/index.spec.ts覆蓋了以下幾個(gè)關(guān)鍵行為可作為使用時(shí)的行為契約只監(jiān)聽 target 上的事件測(cè)試中點(diǎn)擊document.body時(shí)計(jì)數(shù)不變點(diǎn)擊 container 才觸發(fā)驗(yàn)證監(jiān)聽器確實(shí)綁定在目標(biāo)元素上index.spec.ts卸載后自動(dòng)移除unmount()之后再點(diǎn)擊計(jì)數(shù)不再增加驗(yàn)證組件卸載時(shí)監(jiān)聽器被正確清理不會(huì)泄漏index.spec.ts多事件監(jiān)聽click與keydown同時(shí)注冊(cè)、同時(shí)清理index.spec.tsenable 開關(guān)enable為false后即使重新渲染、點(diǎn)擊也不再觸發(fā)驗(yàn)證條件監(jiān)聽生效index.spec.tsref 作為 target 顯式事件泛型useEventListenerscroll(scroll, onScroll, { target })這類帶事件泛型的調(diào)用方式在類型層面受到保護(hù)index.spec.ts。使用建議與注意事項(xiàng)優(yōu)先傳 target 而不是全局監(jiān)聽能用 ref 指向具體元素就盡量指向元素避免在window上掛過(guò)多監(jiān)聽器監(jiān)聽多個(gè)事件時(shí)用數(shù)組一次聲明代碼更簡(jiǎn)潔handler 無(wú)需用 useCallback 包裹得益于useLatest機(jī)制handler 每次渲染重建也不會(huì)導(dǎo)致重復(fù)綁定/解綁這也是該 Hook 相比手寫useEffect加useCallback方案的顯著優(yōu)勢(shì)動(dòng)態(tài)開關(guān)用 enable需要按條件啟用/停用監(jiān)聽時(shí)直接切換enable即可不要用「?jìng)骺諗?shù)組事件名」這類 HackSSR 場(chǎng)景安全在服務(wù)端渲染環(huán)境下 Hook 內(nèi)部會(huì)安全跳過(guò)注冊(cè)不會(huì)因?yàn)閣indow不存在而崩潰依賴 target 元素的掛載時(shí)機(jī)如果目標(biāo)元素是條件渲染的ref 可能在初次渲染時(shí)為空此時(shí)監(jiān)聽器會(huì)跳過(guò)待元素掛載、ref 更新后useEffectWithTarget會(huì)檢測(cè)到 target 變化并自動(dòng)完成綁定無(wú)需額外處理。結(jié)語(yǔ)useEventListener用約 90 行源碼index.ts把原生addEventListener的注冊(cè)、清理、target 解析、條件開關(guān)與類型推導(dǎo)全部抽象成了聲明式的 Hook 調(diào)用。理解它背后useLatest與useEffectWithTarget的組合不僅能幫你用好這個(gè) API也能讓你在面對(duì)「ref 變化時(shí)需要重建副作用」這類常見問(wèn)題時(shí)直接復(fù)用同樣的設(shè)計(jì)思路。相關(guān)示例與測(cè)試均在倉(cāng)庫(kù)packages/hooks/src/useEventListener目錄下可進(jìn)一步閱讀完整源碼加深理解。贊分享前端【免費(fèi)下載鏈接】hooksA high-quality reliable React Hooks library. https://alibaba.github.io/hooks/項(xiàng)目地址https://gitcode.com/gh_mirrors/hooks/hooks點(diǎn)擊查看免費(fèi)下載相關(guān)推薦MonST3R項(xiàng)目如何實(shí)現(xiàn)動(dòng)態(tài)場(chǎng)景的實(shí)時(shí)三維重建MonST3R項(xiàng)目如何實(shí)現(xiàn)動(dòng)態(tài)場(chǎng)景的實(shí)時(shí)三維重建 在動(dòng)態(tài)場(chǎng)景三維重建領(lǐng)域傳統(tǒng)方法常面臨運(yùn)動(dòng)物體干擾、相機(jī)位姿估計(jì)不準(zhǔn)確等挑戰(zhàn)。MonST3R通過(guò)創(chuàng)新的前饋式架前端告別繁瑣復(fù)制用React Hooks封裝clipboard.js的優(yōu)雅實(shí)踐告別繁瑣復(fù)制用React Hooks封裝clipboard.js的優(yōu)雅實(shí)踐 你是否還在為實(shí)現(xiàn)復(fù)制功能編寫冗長(zhǎng)的原生JavaScript代碼是否遇到過(guò)兼容性問(wèn)前端深度探索Android Studio中文語(yǔ)言包插件的3個(gè)高效配置策略深度探索Android Studio中文語(yǔ)言包插件的3個(gè)高效配置策略 Android Studio中文語(yǔ)言包插件為開發(fā)者提供了完整的IDE中文界面支持讓中國(guó)開前端上一篇PKHeX.Mobile權(quán)限與安全指南正確處理Android/iOS存儲(chǔ)和相機(jī)權(quán)限下一篇RR項(xiàng)目為RS4017xs設(shè)備構(gòu)建定制化系統(tǒng)鏡像的技術(shù)實(shí)踐創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考