核心是用IntersectionObserver监听区块进入视口,当进入比例≥50%时,匹配其id与导航href的哈希值(如#about),并添加active类;需防抖、销毁旧观察、路由切换后手动同步、SSR需hydration后初始化。
核心是监听 window.onscroll 或 IntersectionObserver,对比每个内容区块的 offsetTop 与当前 window.scrollY,找出当前可视区域最靠近顶部的区块,再匹配其 id 和对应导航链接的 href 值(如 #about → id="about")。
用 IntersectionObserver 更稳:它不卡主线程、支持阈值控制、自动处理懒加载区块。但 IE 不支持,需降级到 scroll + throttle。
scrollY === offsetTop —— 用户滚动停在中间时永远不匹配id,且确保导航 a[href^="#"] 的哈希值和 id 完全一致(大小写、连字符都不能错)因为语义明确、原生可访问、无需额外维护映射关系。屏幕阅读器会把 a[href="#contact"] 自动关联到 id="contact" 元素,aria-current="section" 也能被正确识别。
用 data-nav-target="contact" 看似灵活,但容易出现拼写不一致、漏加、JS 未加载时失效等问题,且破坏了锚点跳转的天然能力。
立即学习“前端免费学习笔记(深入)”;
#faq 必须对应 <section id="faq">,不能是 <section id="FAQ">
/page#features,要先用 new URL(href).hash 提取 #features,再去掉 #
javascript:void(0) —— 这会让锚点逻辑彻底失效默认 threshold: [0] 会导致刚进入就触发、稍微一晃就反复切换。应设为 [0.1, 0.5, 0.9] 并只在「进入比例 ≥ 50%」时标记为活跃项。
同时加防抖:用 setTimeout 缓存最后一次触发,延迟 50ms 再执行高亮更新,避免快速滚动时 DOM 频繁重绘。
.nav-link.active,再只给匹配项加observer.observe() 里重复注册 —— 初始化一次就够了observer.unobserve(el) 销毁不需要监听的旧区块(比如 SPA 路由切换后)不自动生效。页面没刷新,IntersectionObserver 不会重新计算,而且新内容区块可能还没渲染完。
必须在路由就绪后手动触发一次高亮同步:Vue Router 用 router.afterEach,React Router 用 useEffect 监听 location.pathname,然后调用你的高亮函数。
DOMContentLoaded —— SPA 的 DOM 是动态插入的scrollIntoView({ behavior: 'smooth' }),要在它完成后再高亮,否则可能因滚动未结束而误判滚动监测高亮看着简单,真正落地时最容易栽在「边界判定时机」和「SPA 生命周期脱节」上——前者导致高亮错位,后者导致路由切换后完全失灵。