侧边目录自动滚动应使用IntersectionObserver监听章节容器而非标题,设threshold为0.6并结合getBoundingClientRect校验可见性,统一坐标系;目录动态更新需MutationObserver监听正文变动;侧边栏滚动失效需用子元素padding替代父容器padding。

侧边目录自动滚动不是靠“监听 scrollTop 然后算哪个标题离顶部最近”就能稳的——那样会高频误判、跳变、卡顿,尤其在动态加载或标题密集时。真正能落地的方案,是用 IntersectionObserver 观察章节容器,配合 getBoundingClientRect() 做边界校验,再节流更新高亮状态。
为什么 IntersectionObserver 观察 <h2> 会出问题
直接让 IntersectionObserver 监听所有 <h2> 元素,看似简单,实则失效率高:标题本身高度小(常只有 20–30px),刚滚进视口就触发 isIntersecting: true,但用户实际还没看到正文;如果页面有吸顶导航栏,rootMargin 很难统一适配;更麻烦的是,Markdown 渲染后标题可能被包裹在 <div class="markdown"> 里,DOM 结构不干净,观察目标错位。
✅ 正确做法是:把每个章节内容包裹进 <section>(或带唯一 id 的 <div>),然后观察这些容器。判断“当前章节”的依据是:该容器在视口内可见区域是否 ≥ 60%(不是像素值,是比例)。
- 给每个
<section id="sec-intro">添加data-toc-id="sec-intro",方便和目录项关联 -
IntersectionObserver配置{ threshold: [0.6] },只在可见比例达 60% 时回调 - 回调中遍历所有
entries,取第一个isIntersecting === true的 entry,它的target.dataset.tocId就是当前高亮项
滚动时目录项高亮不同步?检查 top 和 offsetTop 的基准差异
目录点击跳转用 element.scrollIntoView({ block: 'start', behavior: 'smooth' }),但滚动后高亮没变,大概率是“当前章节”判定逻辑和跳转逻辑用了两套坐标系:前者基于 IntersectionObserver(相对视口),后者基于 offsetTop(相对文档)。两者在有固定头部、缩放、字体加载延迟等场景下会偏差几十像素。
立即学习“前端免费学习笔记(深入)”;
✅ 解决办法是统一用 getBoundingClientRect() 校验:
- 在滚动监听回调里,不直接信
entry.isIntersecting,而是补一句:const rect = entry.target.getBoundingClientRect(); if (rect.top window.innerHeight * 0.2) - 这个范围(顶部在视口前 40% 内、底部在视口后 20% 内)比单纯看
isIntersecting更抗干扰 - 避免用
document.documentElement.scrollTop做计算——它在 iOS Safari 中可能不准,window.pageYOffset更可靠
目录生成后,正文增删标题,目录不更新?MutationObserver 没配对
很多脚本只在 DOMContentLoaded 时跑一次 document.querySelectorAll('h2, h3'),之后 Markdown 异步渲染新标题、或者用户用 JS 插入内容,目录就彻底失联。这不是功能缺陷,是设计遗漏。
✅ 必须用 MutationObserver 监听正文容器(比如 <article class="content">)的子节点变动:
- 观察选项设为
{ childList: true, subtree: true },确保新增/删除/移动标题都能捕获 - 回调里只处理
addedNodes,过滤出含<h2>~<h4>的节点,再调用目录重建函数 - 重建前先清空旧目录项,但保留容器 DOM 节点,避免重绘闪烁
- 提取标题文本必须用
node.textContent,不能用innerHTML——否则<code>fetch()</code>会原样塞进目录项里
侧边栏本身滚动失效?别在 .sidenav 上写 padding-top
常见写法:.sidenav { height: 496px; padding-top: 60px; overflow-y: auto; }。看起来没问题,但 padding-top 不参与滚动区域计算,真实可用高度只剩 436px;当目录项展开(比如点击“性能优化”展开子项),内容高度超限,overflow-y: auto 却不显示滚动条——因为浏览器认为“没溢出”,只是 padding 把内容往下挤了。
✅ 正解是分层:让 .sidenav 只管滚动容器职责,内边距交给子元素:
.sidenav {
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
}
.sidenav > .toc-inner {
padding-top: 60px;
}这样 .sidenav 的滚动上下文完全由内容撑开,.toc-inner 的 padding 只影响内部布局,不干扰溢出判定。移动端也一样适用——iOS Safari 对 height: 100vh 的解析不稳定,100% 更可靠。



















