合并自定义React滚动Hooks:获取滚动方向与百分比并优化触发时机的技术咨询
合并滚动方向与百分比的自定义Hook方案
针对你的两个需求——减少不必要的逻辑触发和合并两个Hook,我整理了一个优化后的合并方案,同时解决这两个问题:
核心思路
- 单一事件监听:只绑定一次
scroll事件,在同一个回调里同时计算滚动百分比和方向,避免重复监听导致的性能浪费 - 方向变化才更新:只有当滚动方向确实发生改变时,才更新方向状态,避免无意义的状态更新
- 移除冗余依赖:修复原
useScrollPercentage中不必要的scrollPercentage依赖,避免重复绑定/解绑事件
合并后的Hook代码:useScrollMetrics.js
import { useRef, useState, useEffect } from "react"; import _ from 'lodash'; export default function useScrollMetrics({ threshold = 10, debounce = 10, scrollHeightThreshold = 0 } = {}) { const scrollRef = useRef(null); const [scrollPercentage, setScrollPercentage] = useState(0); const [scrollDirection, setScrollDirection] = useState(null); // 防抖更新方向,避免频繁切换 const debouncedSetDirection = _.debounce(setScrollDirection, debounce); // 计算滚动百分比的工具函数 const getScrollPercentage = (element) => { if (!element) return 0; const height = element.scrollHeight - element.clientHeight; return height > 0 ? Math.round((element.scrollTop / height) * 100) : 0; }; useEffect(() => { const node = scrollRef.current; if (!node) return; let lastScrollY = node.scrollTop; let lastScrollDir = null; let ticking = false; // 检查是否满足滚动高度阈值 const hasEnoughScrollHeight = () => { return node.scrollHeight - node.clientHeight > scrollHeightThreshold; }; const updateScrollMetrics = () => { // 1. 更新滚动百分比 const currentPercentage = getScrollPercentage(node); setScrollPercentage(currentPercentage); // 2. 计算并更新滚动方向(仅当方向变化且满足条件时) const scrollY = node.scrollTop; if (Math.abs(scrollY - lastScrollY) < threshold || !hasEnoughScrollHeight()) { ticking = false; return; } const newScrollDir = scrollY > lastScrollY ? 'Down' : 'Up'; if (newScrollDir !== lastScrollDir) { debouncedSetDirection(newScrollDir); lastScrollDir = newScrollDir; } lastScrollY = Math.max(scrollY, 0); ticking = false; }; const onScroll = () => { if (!ticking) { window.requestAnimationFrame(updateScrollMetrics); ticking = true; } }; // 初始化百分比 setScrollPercentage(getScrollPercentage(node)); // 绑定滚动事件 node.addEventListener('scroll', onScroll, { passive: true }); return () => { node.removeEventListener('scroll', onScroll); debouncedSetDirection.cancel(); // 清理防抖定时器 }; }, [threshold, debounce, scrollHeightThreshold]); // 仅依赖配置项,避免重复执行 return { scrollRef, scrollPercentage, scrollDirection }; }
使用方式(App.js)
const { scrollRef, scrollPercentage, scrollDirection } = useScrollMetrics({ threshold: 10, // 可选:方向切换的最小滚动距离 debounce: 10, // 可选:方向更新的防抖时间 scrollHeightThreshold: 0 // 可选:触发方向检测的最小滚动高度差 }); // 如果你需要在方向变化时执行逻辑,可以用useEffect监听 useEffect(() => { if (scrollDirection) { console.log('滚动方向变化:', scrollDirection); // 这里放你需要在方向变化时触发的逻辑 } }, [scrollDirection]); // 滚动时的统一处理(可选) const handleScroll = () => { console.log('当前滚动百分比:', scrollPercentage, '当前方向:', scrollDirection); }; return ( <Modal ref={scrollRef} onScroll={handleScroll}> // Scrollable Content </Modal> );
关键优化点说明
- 避免重复事件绑定:原
useScrollPercentage的useEffect依赖了scrollPercentage,导致每次百分比变化都会重新绑定/解绑事件,现在改为仅依赖配置参数,只执行一次绑定 - 方向变化才触发更新:只有当新计算的方向和上一次方向不同时,才会调用防抖后的状态更新,避免了每次滚动都触发方向相关逻辑
- 统一事件处理:所有滚动相关的计算都在同一个
requestAnimationFrame回调中完成,性能更优 - 完善的清理逻辑:组件卸载时不仅移除事件监听,还会取消防抖定时器,避免内存泄漏
内容的提问来源于stack exchange,提问作者dev
相关产品推荐
相关产品推荐

