<p>应使用 document.documentElement.scrollTop 获取滚动高度,fallback 到 body.scrollTop;可滚动高度为 document.documentElement.scrollHeight - window.innerHeight;再计算百分比并节流渲染。</p>

怎么用 window.onscroll 监听滚动并计算阅读进度
核心是拿到当前滚动高度和整个可滚动高度,再算百分比。别直接用 document.body.scrollTop,它在某些浏览器(比如 Safari)里可能为 0 —— 应该优先用 document.documentElement.scrollTop,再 fallback 到 body.scrollTop。
可滚动高度也不是 document.body.scrollHeight 就完事:如果页面有 doctype(现代 HTML 都有),实际滚动容器是 document.documentElement,所以要用 document.documentElement.scrollHeight - document.documentElement.clientHeight。
实操建议:
- 用
window.innerHeight替代document.documentElement.clientHeight更稳妥(后者在某些 zoom 场景下会失准) - 监听前加节流(比如用
requestAnimationFrame或 50ms debounce),否则快速滚动时会频繁触发重绘 - 进度值建议保留 1 位小数,避免反复渲染 99.999% → 100% 这种抖动
CSS 实现顶部固定进度条的常见坑
很多人用 position: fixed + top: 0,结果发现进度条被 sticky 导航栏或 z-index 更高的弹窗盖住。这不是 bug,是层叠上下文没理清。
立即学习“前端免费学习笔记(深入)”;
关键点:
- 必须给进度条设
z-index: 9999以上(别信“999 够了”,有些 UI 框架默认 z-index 就到 2000+) - 不要依赖父容器的
transform或will-change,它们会创建新层叠上下文,把进度条锁死在局部层级里 - 宽度用
width: calc(100% - 2 * env(safe-area-inset-left))兼容 iOS 安全区,否则刘海屏下进度条左右被切掉 - 动画用
transform: scaleX()而不是width,避免重排
Vue / React 里怎么避免重复绑定 scroll 事件
组件卸载时没清理 window.onscroll,会导致内存泄漏和跨路由后进度条错乱。尤其在 SPA 中,用户来回切文章页,不清理就会叠加多个监听器。
实操要点:
- Vue 3 Composition API:在
onBeforeUnmount里调用window.removeEventListener('scroll', handler) - React:
useEffect的 cleanup 函数里移除监听,且确保 handler 是稳定引用(用useCallback包一层) - 别在
mounted/useEffect里直接写匿名函数当监听器,否则 remove 时找不到原函数 - 如果用了第三方库(如
vue-use的useScroll),确认它内部是否自动清理 —— 很多版本默认不清理
为什么 IntersectionObserver 不适合做阅读进度
有人想用它监听段落进入视口来累加进度,逻辑看似合理,但实际误差极大:长段落、图片懒加载、动态插入内容都会让统计失真。更关键的是,它根本没法反映“用户到底看到哪了”,只反映“哪些块曾经进过 viewport”。
典型问题:
- 用户快速滚动时,很多
IntersectionObserver回调还没触发就已离开,导致进度跳变甚至倒退 - 响应式布局下,同一段落在不同屏幕尺寸中占视口比例不同,无法线性映射到整体进度
- 遇到
position: sticky或transform的容器,observer 的 rootBounds 计算会出错
真正要精准,还是得回到 scroll 位置 + 页面总高这个原始公式。复杂点在于处理各种 DOM 可滚动根节点的判断逻辑,这点最容易被忽略。



















