React 自定义 Hook 实现滚动事件监听
在 React 开发中,常常需要监听滚动事件,比如:
- 滚动到顶部/底部时触发回调
- 判断元素是否可滚动
- 滚动时触发动画或加载数据
本篇文章将带来一个 高性能且易用的 useScroll
自定义 Hook,并结合 lodash
的防抖(debounce)优化性能,同时详细解析每个步骤和原理。
📦 Hook 功能简介
useScroll
支持以下功能:
- 监听
scroll
事件 - 滚动到顶部或底部时触发回调
- 支持元素或
document
滚动 - 自动检测元素是否可滚动
- 可配置防抖时间、偏移量等
✏️ 完整代码
下面是 完整代码
复制
import { useState, useEffect, useRef, useCallback } from "react"
import debounce from "lodash/debounce"
interface UseScrollProps {
ele: React.RefObject<HTMLElement | HTMLBodyElement>
onNonExistScroll?: () => boolean
onScroll?: (e: Event) => void
onScrollToBottom?: () => void
onScrollToTop?: () => void
debounceTimeOut?: number
debounceOptions?: { leading?: boolean; trailing?: boolean }
offsetValue?: number
}
export function useScroll({
ele,
onNonExistScroll,
onScroll,
onScrollToBottom,
onScrollToTop,
debounceTimeOut = 100,
debounceOptions,
offsetValue = 10,
}: UseScrollProps) {
const timerRef = useRef<number>()
const testScrollExistCount = useRef(0)
const [existScroll, setExistScroll] = useState(false)
const handleOnScrollToBottom = useCallback(() => {
onScrollToBottom && onScrollToBottom()
}, [onScrollToBottom])
const handleOnScrollToTop = useCallback(() => {
onScrollToTop && onScrollToTop()
}, [onScrollToTop])
const handleOnScroll = useCallback(
debounce((e: Event) => {
if (!e.target || !ele.current) return
const target = ele.current instanceof HTMLBodyElement
? document.scrollingElement as HTMLElement
: e.target as HTMLElement
onScroll && onScroll(e)
const { clientHeight, scrollTop, scrollHeight } = target
if (scrollHeight - scrollTop <= clientHeight + offsetValue) {
handleOnScrollToBottom()
}
if (scrollTop === 0) {
handleOnScrollToTop()
}
}, debounceTimeOut, debounceOptions),
[debounceTimeOut, debounceOptions, ele, offsetValue, handleOnScrollToBottom, handleOnScrollToTop, onScroll]
)
const testScrollExist = useCallback(() => {
if (!ele.current) return
testScrollExistCount.current += 1
if (testScrollExistCount.current > 5 && timerRef.current) {
clearInterval(timerRef.current)
}
let newExistScroll = false
if (ele.current instanceof HTMLBodyElement) {
newExistScroll = ele.current.scrollHeight > window.innerHeight
} else {
const { clientHeight, scrollHeight } = ele.current
newExistScroll = scrollHeight > clientHeight
}
if (newExistScroll) {
timerRef.current && clearInterval(timerRef.current)
} else {
if (onNonExistScroll) {
const stop = onNonExistScroll()
if (stop) timerRef.current && clearInterval(timerRef.current)
}
}
setExistScroll(newExistScroll)
}, [ele, onNonExistScroll])
useEffect(() => {
if (ele.current) {
timerRef.current = window.setInterval(testScrollExist, 1000)
}
return () => {
timerRef.current && clearInterval(timerRef.current)
}
}, [ele, testScrollExist])
useEffect(() => {
const target = ele.current
if (!target) return
if (target instanceof HTMLBodyElement) {
document.addEventListener("scroll", handleOnScroll)
} else {
target.addEventListener("scroll", handleOnScroll)
}
return () => {
if (target instanceof HTMLBodyElement) {
document.removeEventListener("scroll", handleOnScroll)
} else {
target.removeEventListener("scroll", handleOnScroll)
}
}
}, [ele, handleOnScroll])
return { existScroll }
}
🧠 核心思路解析
1. 判断滚动目标
支持监听 document
(<body>
)或具体 DOM 元素。
2. 防抖
使用 lodash/debounce
防止滚动事件高频触发,提高性能。
3. 判断滚动是否到达底部/顶部 根据:
scrollHeight - scrollTop <= clientHeight + offsetValue
(底部)scrollTop === 0
(顶部)
4. 判断元素是否可滚动
根据 scrollHeight > clientHeight
(元素)或 scrollHeight > window.innerHeight
(文档)。
🧩 使用示例
复制
const scrollRef = useRef<HTMLDivElement>(null)
const { existScroll } = useScroll({
ele: scrollRef,
onScroll: () => console.log("Scrolling..."),
onScrollToBottom: () => console.log("Reached bottom!"),
onScrollToTop: () => console.log("Reached top!"),
debounceTimeOut: 200,
})
🌱 总结
通过一个简单的自定义 Hook,就能轻松实现滚动监听、到顶/底检测、滚动存在性检测等常见需求。再配合防抖处理,就能让滚动监听既高效又易用。
作者:https://blog.xn--rpv331d.com/望舒
链接:https://blog.xn--rpv331d.com/望舒/blog/83
转载注意保留文章出处...
No data