React Hooks疑问:滚动时如何正确设置lastScroll及计算scrollSpeed?
我来帮你拆解这几个问题的核心原因,都是React状态更新机制和闭包特性导致的,咱们一步步理清楚:
核心根源:过时闭包 + React状态更新的异步性
你的useEffect依赖数组是空的[],这意味着handleScroll函数只会在组件挂载时创建一次。它内部捕获的docScroll、lastScroll都是组件初始渲染时的状态值——之后无论状态怎么更新,这个函数里的变量都不会同步最新值,这就是过时闭包问题。同时,React的状态更新是异步批量处理的,在同一个事件回调里,你无法立即拿到刚更新的状态值。
逐个解答你的疑问
1. 为什么setLastScroll(getScroll())能“正常”保存上一次滚动值?
其实这是一种巧合的表现:
你直接用getScroll()从DOM获取实时滚动值,绕过了闭包里过时的状态变量。第一次滚动时,docScroll和lastScroll都会被设为当前滚动值;第二次滚动时,组件已经因为第一次状态更新重新渲染,此时docScroll是上一次的滚动值,而getScroll()获取的是最新值,所以lastScroll会被设为最新值,看起来像是“保存了上一次”——但本质上这是依赖组件重新渲染后的状态同步,不是正确的逻辑。
2. 为什么setLastScroll(docScroll)时,lastScroll始终为0?
因为handleScroll是在useEffect([])里创建的,它捕获的docScroll是页面刚加载时的初始值(比如0)。之后无论docScroll怎么更新,这个函数里的docScroll永远都是初始的0,所以每次执行setLastScroll(docScroll)都是把lastScroll设为0。
3. 为什么setScrollSpeed(Math.abs(docScroll - lastScroll))无效?
同样是过时闭包的锅:handleScroll里的docScroll和lastScroll都是初始值(比如0和null),所以计算出来的scrollSpeed永远是0或者NaN,根本不会随着滚动更新。
修复方案:用useRef跟踪状态,避免闭包问题
要正确实现“当前滚动值、上一次滚动值、滚动速度”的逻辑,最可靠的方式是用useRef存储上一次的滚动值——因为ref的.current属性是可变的,不受闭包限制,也不会触发组件重新渲染。
修复后的完整代码:
function useDocScroll() { const isClient = typeof window === "object"; // 获取当前滚动值的工具函数 const getScroll = () => { return isClient ? window.pageYOffset || document.documentElement.scrollTop : 0; }; // 状态变量:当前滚动值、上一次滚动值、滚动速度 const [docScroll, setDocScroll] = useState(getScroll()); const [lastScroll, setLastScroll] = useState(getScroll()); const [scrollSpeed, setScrollSpeed] = useState(0); // 用ref存储上一次滚动值,避免闭包问题 const lastScrollRef = useRef(getScroll()); useEffect(() => { if (!isClient) return; const handleScroll = () => { const currentScroll = getScroll(); // 计算滚动速度:当前值与上一次值的差的绝对值 const speed = Math.abs(currentScroll - lastScrollRef.current); // 更新状态 setDocScroll(currentScroll); setLastScroll(lastScrollRef.current); setScrollSpeed(speed); // 更新ref的当前值,供下一次滚动时使用 lastScrollRef.current = currentScroll; }; window.addEventListener("scroll", handleScroll); // 组件卸载时移除监听,避免内存泄漏 return () => window.removeEventListener("scroll", handleScroll); }, []); // 依赖为空数组即可,因为用的是ref,不受闭包影响 return [docScroll, lastScroll, scrollSpeed]; }
修复逻辑说明:
- 用
useRef存储上一次的滚动值,每次滚动时直接读取.current属性获取最新的历史值。 - 在
handleScroll里,先获取当前滚动值,再计算速度,最后同步更新三个状态变量。 - 最后更新
lastScrollRef.current为当前值,确保下一次滚动时能拿到正确的历史值。
总结
你的核心问题都是源于对React闭包特性和状态更新异步性的不熟悉:
- 不要在依赖为空的
useEffect回调中直接使用状态变量,它们会永远停留在初始值。 - 如果需要跟踪状态的历史值,
useRef是比状态变量更可靠的选择,因为它不受闭包限制。 - 状态更新是异步的,不要在同一个事件回调里依赖刚更新的状态值做计算。
内容的提问来源于stack exchange,提问作者Jaye Park

