⭐ 推荐:社区规则条款 V1.0
x123456

x123456

见习用户见习用户
加入于 2026-10-11
0主题 2回复 0签到 0连续 0关注 0粉丝
回复于 2026-10-11 23:09 来自 HTML 锚点跳转平滑滚动的 3 种实现方式

方向我完全同意,但「IO 判可见比例最高」这个判据本身在滚动中会抖,我一般不用它。

判定带比比例更稳:IntersectionObserver 的 threshold 只在越界时回调,滚动过程里拿不到实时的「谁占比最高」,而且首节/末节天生吃亏——滚到底时最后一节可能只占视口 30%,高亮就永远跳不过去。直接压一条判定带更省事:rootMargin: '-80px 0px -60% 0px'(上边避顶栏,下边砍掉 60%),谁进入这条带谁高亮,不用算比例。末节到不了位就在滚动到底时强制点亮最后一个,一行判断的事。

scrollend 有个死角:点当前已在位置的那一节时,scrollTo 的目标和当前完全一致,浏览器压根不产生滚动,scrollend 不会触发,isAnimating 就永久卡 true,高亮从此死掉。稳妥做法是 scrollTo 之前先比对 Math.abs(currentTop - targetTop) < 2 就直接跳过不清标志位。另外打断场景下,滚动停止本身通常也会触发一次 scrollend,所以你补的 wheel/touchstart 清理更多是给老浏览器的超时兜底路径用的,两条都留着不冲突,只是别指望它俩是主路径。

container.scrollTo({ top: el.offsetTop }) 的隐含前提是容器的定位。容器一旦是 static,el.offsetTop 是相对外面那个定位祖先算的,数会直接飘到姥姥家——这也是为什么「容器 position: relative」这个前提必须写进注释。想稳一点就判一下 el.offsetParent === container,不等就退回 rect 那套。

scrollRestoration = 'manual' 记得配套做列表页位置恢复,否则后退回去用户在长列表里直接懵,等于把坑从详情页挪到了列表页。

回复于 2026-10-11 22:51 来自 HTML 锚点跳转平滑滚动的 3 种实现方式

这几条补得很扎实,尤其 prefers-reduced-motion 那半截——只写 CSS 是拦不住的,JS 触发的平滑动画照样跑。

无障碍要前后端一起管。CSS 媒体查询只能改掉 CSS 触发的滚动,scrollTo({behavior:'smooth'}) 走的是另一条路,得在 JS 里再判一次:

const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
window.scrollTo({ top, behavior: reduce ? 'auto' : 'smooth' });

偏移这件事,我的建议是全部收进 CSS。给目标加 scroll-margin-top(或容器加 scroll-padding-top,二选一),然后用 scrollIntoView({ block: 'start' })——浏览器会自动把这层偏移算进去,JS 里就不用再减顶栏高度。这样一来你提的"80px 别写死"其实也绕过去了:偏移交给 CSS 布局算,移动端顶栏收缩时它跟着变,不用监听 resize。JS 只干两件事:控制 behavior、同步 hash。

hash 同步加一个判断,不然连点同一节会把历史堆满:

if (location.hash !== href) history.pushState(null, '', href);
else history.replaceState(null, '', href);

选择器那条同意,decodeURIComponent(href.slice(1)) 里的解码别漏——中文 id 在 href 里是百分号编码的,直接切取不到;不想手写就用 CSS.escape。

第三种就是 rAF 手写缓动,三个点:wheel/touchstart/keydown 监听加 {passive:true} 并在打断时 cancelAnimationFrame(记得移除监听);缓动用 easeInOutCubic,线性很生硬;hash 在动画起点就同步,别等结束。

延伸一个坑:页面带 hash 首次加载时,html 上的 scroll-behavior: smooth 会让它从顶部"滑"到目标位置,观感很怪。常见做法是脚本执行后再启平滑,或加载瞬间临时置 auto。