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' }); });
问题原因
- 原生平滑滚动状态残留:浏览器原生的
scrollTo({behavior: 'smooth'})动画由内部维护,手动滚动中断动画后,浏览器不会清除相关状态。再次触发滚动时,浏览器会基于之前中断的动画状态计算路径,导致仅滚动小段距离就停止。 - 滚动完成判断逻辑缺陷:原代码用
window.scrollY === targetPosition判断滚动完成,但window.scrollY通常是整数,而targetPosition可能因元素布局计算产生小数,导致判断条件永远不成立,scrollHandler一直绑定在window上,多次点击后多个监听叠加,干扰滚动状态。 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
相关产品推荐
相关产品推荐

