问答题34/1770怎么根据当前滚动位置,实现目录中对应节点自动高亮?

难度:
2026-01-31 创建

参考答案:

“目录中自动高亮”(TOC Active)本质是 根据当前滚动位置,判断用户正在阅读的标题节点,并同步更新目录状态。这是文档类产品、Markdown 阅读器中非常常见的能力。

下面从 设计思路 → 常见实现方案 → 工程细节与坑 三个层次说明。


一、核心设计思路

滚动驱动状态,状态驱动目录高亮

关键问题只有两个:

  1. 如何判断当前“激活”的标题?
  2. 如何高效监听滚动并更新状态?

二、实现方案一(推荐):IntersectionObserver

适用场景

  • 现代浏览器
  • 标题是标准 DOM(h1–h6)

实现思路

  • 监听所有标题元素
  • 哪个标题进入视口上方区域,即认为是当前阅读位置

基本实现示例

1const headings = document.querySelectorAll('h1, h2, h3'); 2 3const observer = new IntersectionObserver( 4 (entries) => { 5 entries.forEach(entry => { 6 if (entry.isIntersecting) { 7 setActiveId(entry.target.id); 8 } 9 }); 10 }, 11 { 12 rootMargin: '0px 0px -70% 0px', 13 threshold: 0 14 } 15); 16 17headings.forEach(h => observer.observe(h));

原理说明

  • rootMargin: -70%:让标题进入页面上半部分就触发
  • isIntersecting 表示元素进入“有效阅读区”
  • activeId 用于目录高亮

优点

  • 性能好(浏览器原生)
  • 不依赖 scroll 事件
  • 不需要手动计算高度

缺点

  • 老浏览器需 polyfill

三、实现方案二:scroll + getBoundingClientRect(传统方案)

实现思路

  1. 监听 scroll
  2. 找到距离顶部最近但未超出的标题

示例代码

1const headings = [...document.querySelectorAll('h1,h2,h3')]; 2 3window.addEventListener('scroll', () => { 4 let active = null; 5 6 for (const h of headings) { 7 const { top } = h.getBoundingClientRect(); 8 if (top <= 100) { 9 active = h; 10 } else { 11 break; 12 } 13 } 14 15 if (active) { 16 setActiveId(active.id); 17 } 18});

优化点

  • requestAnimationFrame 节流
  • 提前缓存 headings
  • 滚动事件加 passive

优缺点

优点缺点
兼容性好性能较差
易理解手写逻辑多

四、实现方案三:基于文档模型(编辑器场景)

如果你是:

  • 使用 markdown-it
  • Slate / ProseMirror

可以直接从 AST / Node 结构获取标题位置

思路

  • 渲染时记录 headingId → offsetTop
  • 滚动时用 scrollTop + 二分查找

五、目录点击与滚动同步(反向联动)

点击目录 → 滚动正文

1function scrollToHeading(id) { 2 document.getElementById(id)?.scrollIntoView({ 3 behavior: 'smooth', 4 block: 'start' 5 }); 6}

注意点

  • 固定头部高度(navbar)
1window.scrollTo({ 2 top: el.offsetTop - headerHeight, 3 behavior: 'smooth' 4});

六、工程级细节 & 常见坑

多个标题同时可见怎么办?

规则:

  • 取「最靠上的一个」
  • 或 IntersectionObserver 中按 boundingClientRect.top 排序

页面初始加载未触发 scroll?

  • 首次执行一次计算
  • 或 IntersectionObserver 自动触发

高亮抖动?

  • 增加缓冲区(rootMargin)
  • 或 debounce 状态更新

锚点重复?

  • 渲染 markdown 时生成 唯一 id
  • title + index 或 hash

七、React / Vue 实战建议

React

  • useEffect 注册 observer
  • activeId 放在 useState
  • 组件卸载时 disconnect

Vue

  • onMounted 注册
  • onUnmounted 清理
  • activeId 用 ref

八、推荐方案总结

场景推荐方案
普通文档IntersectionObserver
老浏览器scroll + rect
编辑器AST + offset

最近更新时间:2026-02-27

赞赏支持

题库维护不易,您的支持就是我们最大的动力!