如何阻止用户中断window.scrollTo()的平滑滚动过程?
解决window.scrollTo平滑滚动时用户中断操作的不稳定问题
你的思路是对的:通过标记isMoving来控制是否阻止用户的滚动操作,但代码里有几个关键问题导致了偶发失效,我来帮你梳理并修复:
核心问题分析
错误阻止了浏览器原生滚动的
scroll事件
你给scroll事件绑定了preventDefault,但浏览器的平滑滚动本身会触发scroll事件,阻止它会打乱浏览器的滚动动画逻辑,导致滚动提前终止、状态混乱,这是偶发失效的主要原因。滚动完成的判断不可靠
用scrollTop === offset来判断滚动结束有风险:浏览器的平滑滚动是分步执行的,可能因为动画精度问题,滚动位置刚好跳过目标值,或者用户操作干扰导致判断逻辑失效。
修复后的代码方案
下面是优化后的实现,解决了上述问题,同时兼顾了兼容性:
let isMoving = false; const scroll_keys = { 37: 1, 38: 1, 39: 1, 40: 1 }; // 保存当前滚动结束的处理函数,避免多次调用冲突 let currentScrollEndHandler = null; const preventDefault = e => { if (!isMoving) return; e = e || window.event; if (e.preventDefault) e.preventDefault(); e.returnValue = false; }; const preventDefaultForScrollKeys = e => { if (!isMoving) return; if (scroll_keys[e.keyCode]) { preventDefault(e); return false; } }; // 只监听用户主动触发的滚动操作事件,不干扰浏览器原生滚动 document.addEventListener('wheel', preventDefault, { passive: false }); document.addEventListener('DOMMouseScroll', preventDefault, { passive: false }); window.ontouchmove = preventDefault; document.onkeydown = preventDefaultForScrollKeys; function scrollTo(offset, callback) { // 如果正在执行滚动,先终止之前的任务 if (isMoving) { window.removeEventListener('scrollend', currentScrollEndHandler); // 立即停止当前滚动 window.scrollTo({ top: window.pageYOffset, behavior: 'auto' }); } isMoving = true; // 使用原生scrollend事件监听滚动结束(推荐,更可靠) currentScrollEndHandler = () => { window.removeEventListener('scrollend', currentScrollEndHandler); currentScrollEndHandler = null; isMoving = false; callback?.(); }; window.addEventListener('scrollend', currentScrollEndHandler); // 触发平滑滚动 window.scrollTo({ top: offset, behavior: 'smooth' }); // 降级处理:针对不支持scrollend的旧浏览器(如Safari <15.4) if (!('onscrollend' in window)) { let lastScrollTop = window.pageYOffset; const checkScrollStopped = () => { // 如果当前任务已被替换,停止检测 if (currentScrollEndHandler !== checkScrollStopped) return; const currentScrollTop = window.pageYOffset; // 判断滚动是否停止:要么到达目标值,要么位置不再变化 if (currentScrollTop === offset || currentScrollTop === lastScrollTop) { window.removeEventListener('scrollend', currentScrollEndHandler); currentScrollEndHandler = null; isMoving = false; callback?.(); return; } lastScrollTop = currentScrollTop; requestAnimationFrame(checkScrollStopped); }; currentScrollEndHandler = checkScrollStopped; requestAnimationFrame(checkScrollStopped); } } // 调用示例 isMoving = true; scrollTo(0, () => { isMoving = false; });
关键优化点
- 移除
scroll事件的阻止逻辑:只拦截用户主动触发的滚动操作(滚轮、方向键、触摸滑动),让浏览器原生的平滑滚动不受干扰。 - 使用
scrollend事件判断滚动完成:这是浏览器原生提供的滚动结束事件,比手动判断scrollTop更准确,能完美捕捉平滑滚动的结束时机。 - 处理多次滚动调用的冲突:保存当前滚动任务的处理函数,新调用时先终止旧任务,避免状态混乱。
- 兼容旧浏览器:对不支持
scrollend的浏览器,用requestAnimationFrame持续检测滚动位置是否停止变化,作为降级方案。
这样修改后,就能稳定阻止用户在平滑滚动完成前中断操作了。
内容的提问来源于stack exchange,提问作者Ben Klingeler
相关产品推荐
相关产品推荐

