禁用 document.body.style.overflow = 'hidden' 不可靠,因其在 ios safari 中被忽略、导致页面跳回顶部且禁用子元素滚动;应改用 position: fixed + top 偏移并配合 touchmove 拦截与 scrolly 精确还原。

直接禁用 document.body.style.overflow 是最常见但最容易出错的做法——它会重置滚动位置、触发页面跳回顶部,且在 iOS Safari 和部分 Android 浏览器中根本无效。
为什么 overflow: hidden 不可靠
设置 document.body.style.overflow = 'hidden' 看似简单,实际有三个硬伤:
- iOS Safari 会忽略该样式,背景仍可滚动
- 页面会瞬间跳转到顶部(
window.scrollY被丢弃) - 若菜单本身需要内部滚动(比如长列表),
overflow: hidden会连带禁掉子元素的touchmove,导致菜单无法滑动
position: fixed + top 偏移才是稳定解法
核心思路是把 body “钉住”,同时手动补偿滚动偏移,不让用户感知位置变化:
- 展开菜单前,先记录当前
window.scrollY(记作top1) - 执行
document.body.style.position = 'fixed',再设document.body.style.top = `-${top1}px` - 关闭时,恢复
position: static,并调用window.scrollTo(0, top1) - 必须配合
body { width: 100%; },否则 fixed 后宽度收缩引发横向滚动条
移动端必须拦截 touchmove 事件
仅靠 CSS 无法阻止 iOS/Android 上的惯性滚动穿透,需显式阻止默认行为:
- 监听
touchmove时传入{ passive: false },否则preventDefault()会被浏览器忽略 - 只在菜单打开时绑定,关闭时立即
removeEventListener,避免全局污染 - 不要无差别阻止所有
touchmove—— 若菜单内有可滚动区域(如长列表),应通过event.target.closest('.scrollable-area')判断是否放行
React/Vue 项目里容易漏掉的兼容点
框架组件生命周期和 DOM 更新时机会影响滚动锁定效果:
- 在
useEffect或mounted中设置锁定逻辑时,确保 DOM 已渲染完成(可用nextTick或setTimeout(() => {}, 0)延后执行) - 如果用了第三方滚动库(如
perfect-scrollbar),它的scrollTop可能不等于window.scrollY,得从对应实例取值 - 多个弹层嵌套时(比如抽屉里再开 Modal),需维护一个计数器,只有最外层关闭才恢复滚动,否则会提前解锁
真正麻烦的不是加锁,而是解锁时能否精确还原 scrollY —— 尤其在快速连续开关菜单、或用户中途手动滚动的情况下,top 值容易错位。建议始终用 document.documentElement.scrollTop || document.body.scrollTop 双取值,而不是只信 window.scrollY。











