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

ScrollTo动画偶尔卡顿的原因及修复方案咨询

问题描述

我的网站设有hero区域,底部带有一个按钮,点击该按钮可自动滚动至hero区域下方的下一个元素。但遇到一个问题:若用户手动滚动中断了滚动动画,再次点击按钮时,滚动动画会出现卡顿现象——有时仅滚动10-50px,而非预期的完整距离。


相关代码

HTML

<div class="hero-arrow background-colour-ease" id="js-hero-arrow">
  <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>
</div>

尝试修复但无效的JS代码

//Hero Arrow Scroll Button
var isScrolling = false;
let B1 = $(".B1");

$(document).on('click', '#js-hero-arrow', function() {

    if (isScrolling === false) {

        isScrolling = true;

        scrollToElementTop(B1).then(() => {
            isScrolling = false;
        });

    }
        
});

//Function to add a callback to the scroll element top
function scrollToElementTop (elem) {

    const targetPosition = (elem.offset().top + elem.outerHeight(true));

    window.scrollTo({
        top: targetPosition,
        behavior: 'smooth'
    });

    return new Promise(resolve => {
        const scrollHandler = () => {
            if (window.scrollY === targetPosition) {
                window.removeEventListener('scroll', scrollHandler)
                resolve();
            }
        }
        window.addEventListener('scroll', scrollHandler)
    });
}

无临时修复的原始JS代码

//Hero Arrow Scroll Button
let B1 = $(".B1");

$(document).on('click', '#js-hero-arrow', function() {

    const targetPosition = B1.offset().top + B1.outerHeight(true);

    window.scrollTo({
        top: targetPosition,
        behavior: 'smooth'
    });
        
});

问题原因

  1. 原生平滑滚动状态残留:浏览器原生的scrollTo({behavior: 'smooth'})动画由内部维护,手动滚动中断动画后,浏览器不会清除相关状态。再次触发滚动时,浏览器会基于之前中断的动画状态计算路径,导致仅滚动小段距离就停止。
  2. 滚动完成判断逻辑缺陷:原代码用window.scrollY === targetPosition判断滚动完成,但window.scrollY通常是整数,而targetPosition可能因元素布局计算产生小数,导致判断条件永远不成立,scrollHandler一直绑定在window上,多次点击后多个监听叠加,干扰滚动状态。
  3. isScrolling变量无法重置:由于判断条件不成立,Promise无法resolve,isScrolling一直为true,后续点击本应被拦截,但手动滚动触发scroll事件的频率高,易出现逻辑漏洞,导致多次触发不完整滚动。

修复方案

方案1:快速中断现有滚动(最简单)

通过先立即滚动到当前位置,强制中断任何正在进行的平滑滚动,再执行目标滚动:

let B1 = $(".B1");

$(document).on('click', '#js-hero-arrow', function() {
    // 立即滚动到当前位置,中断现有平滑动画
    window.scrollTo({
        top: window.scrollY,
        behavior: 'instant'
    });
    
    // 执行目标滚动
    const targetPosition = B1.offset().top + B1.outerHeight(true);
    window.scrollTo({
        top: targetPosition,
        behavior: 'smooth'
    });
});

方案2:改进滚动状态判断(保留原生平滑效果)

优化滚动完成的判断逻辑,增加误差范围和超时兜底,避免监听函数残留:

var isScrolling = false;
let B1 = $(".B1");
const SCROLL_TOLERANCE = 5; // 滚动误差容忍范围
const SCROLL_TIMEOUT = 3000; // 超时兜底时间

$(document).on('click', '#js-hero-arrow', function() {
    if (isScrolling) return;

    isScrolling = true;
    scrollToElementTop(B1).then(() => {
        isScrolling = false;
    });
});

function scrollToElementTop(elem) {
    const targetPosition = elem.offset().top + elem.outerHeight(true);
    let lastScrollY = window.scrollY;

    window.scrollTo({
        top: targetPosition,
        behavior: 'smooth'
    });

    return new Promise((resolve) => {
        const scrollHandler = () => {
            // 判断是否接近目标位置
            const isAtTarget = Math.abs(window.scrollY - targetPosition) <= SCROLL_TOLERANCE;
            // 判断是否是手动滚动(滚动幅度过大,视为中断)
            const isManualScroll = Math.abs(window.scrollY - lastScrollY) > 10;
            lastScrollY = window.scrollY;

            if (isAtTarget || isManualScroll) {
                window.removeEventListener('scroll', scrollHandler);
                resolve();
            }
        };

        window.addEventListener('scroll', scrollHandler);
        // 超时兜底,防止监听函数永久残留
        setTimeout(() => {
            window.removeEventListener('scroll', scrollHandler);
            resolve();
        }, SCROLL_TIMEOUT);
    });
}

方案3:自定义滚动动画(可控性最强)

放弃原生平滑滚动,用requestAnimationFrame实现自定义动画,完全控制滚动过程:

let B1 = $(".B1");
let isScrolling = false;
const SCROLL_DURATION = 800; // 滚动时长(毫秒)

$(document).on('click', '#js-hero-arrow', function() {
    if (isScrolling) return;

    const targetPosition = B1.offset().top + B1.outerHeight(true);
    const startPosition = window.scrollY;
    const distance = targetPosition - startPosition;
    let startTime = null;

    isScrolling = true;

    // 缓动函数,模拟原生平滑效果
    function ease(progress) {
        return progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
    }

    function animation(currentTime) {
        if (!startTime) startTime = currentTime;
        const timeElapsed = currentTime - startTime;
        const progress = Math.min(timeElapsed / SCROLL_DURATION, 1);
        const scrollY = startPosition + distance * ease(progress);

        window.scrollTo(0, scrollY);

        if (progress < 1) {
            requestAnimationFrame(animation);
        } else {
            isScrolling = false;
            window.removeEventListener('scroll', interruptHandler);
        }
    }

    // 监听手动滚动,中断动画
    const interruptHandler = () => {
        isScrolling = false;
        window.removeEventListener('scroll', interruptHandler);
    };
    window.addEventListener('scroll', interruptHandler);

    requestAnimationFrame(animation);
});

内容的提问来源于stack exchange,提问作者MailCarrier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 22:25:58