You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

合并自定义React滚动Hooks:获取滚动方向与百分比并优化触发时机的技术咨询

合并滚动方向与百分比的自定义Hook方案

针对你的两个需求——减少不必要的逻辑触发和合并两个Hook,我整理了一个优化后的合并方案,同时解决这两个问题:

核心思路

  1. 单一事件监听:只绑定一次scroll事件,在同一个回调里同时计算滚动百分比和方向,避免重复监听导致的性能浪费
  2. 方向变化才更新:只有当滚动方向确实发生改变时,才更新方向状态,避免无意义的状态更新
  3. 移除冗余依赖:修复原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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 14:34:07