Skip to main content

滑动穿透

一、滑动穿透的本质

滑动穿透指上层元素(如弹窗、浮层)滚动时,下层页面也随之滚动的现象。其核心原因是:

  1. 事件冒泡touchmove 事件从上层元素冒泡到下层。
  2. 默认行为未被阻止:浏览器默认处理滚动,即使在不需要滚动的区域。
  3. 滚动容器嵌套:多层可滚动元素导致事件处理冲突。

二、解决方案分类

根据场景和兼容性需求,可选择以下方案:

方案一:全局禁用滚动(简单粗暴)

原理:显示浮层时禁用整个页面滚动,隐藏时恢复。
优点:实现简单,兼容性好。
缺点:滚动位置可能丢失,iOS 体验不佳。

// 显示浮层时
function showOverlay() {
// 保存当前滚动位置
const scrollTop = window.scrollY;

// 禁用页面滚动(关键)
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
document.body.style.width = '100%';
}

// 隐藏浮层时
function hideOverlay() {
const scrollTop = parseInt(document.body.style.top);

// 恢复页面滚动
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.top = '';

// 恢复滚动位置
window.scrollTo(0, -scrollTop);
}

方案二:局部滚动控制(精细化)

原理:通过事件检测,仅在需要时阻止滚动。
优点:保留页面滚动状态,体验更流畅。
缺点:实现复杂,需处理边界情况。

let startY, scrollTop;

// 显示浮层时
function showOverlay() {
scrollTop = window.scrollY;

// 监听触摸事件
document.addEventListener('touchstart', handleTouchStart);
document.addEventListener('touchmove', handleTouchMove, { passive: false });
}

// 隐藏浮层时
function hideOverlay() {
document.removeEventListener('touchstart', handleTouchStart);
document.removeEventListener('touchmove', handleTouchMove);
}

function handleTouchStart(e) {
startY = e.touches[0].clientY;
}

function handleTouchMove(e) {
const currentY = e.touches[0].clientY;
const diffY = currentY - startY;

// 容器元素(如弹窗内容区)
const container = document.querySelector('.overlay-content');
const containerScrollTop = container.scrollTop;
const containerHeight = container.offsetHeight;
const containerScrollHeight = container.scrollHeight;

// 向上滑动且容器已滑到顶部,阻止继续滑动
if (diffY > 0 && containerScrollTop <= 0) {
e.preventDefault();
}
// 向下滑动且容器已滑到底部,阻止继续滑动
else if (diffY < 0 && containerScrollTop >= containerScrollHeight - containerHeight) {
e.preventDefault();
}
// 容器可滚动时,不阻止
}

方案三:CSS 方案(现代浏览器)

原理:利用 touch-action 属性控制触摸行为。
优点:性能最优,代码简洁。
缺点:iOS Safari 兼容性差(需配合 JS)。

/* 全局样式 */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
touch-action: none; /* 关键:禁用默认触摸行为 */
}

/* 可滚动内容区 */
.overlay-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow-y: auto;
touch-action: pan-y; /* 允许垂直滚动 */
-webkit-overflow-scrolling: touch; /* iOS 滚动优化 */
}

方案四:混合方案(最佳实践)

结合 CSS 和 JS,兼顾性能与兼容性

// 显示浮层时
function showOverlay() {
// CSS 禁用默认滚动
document.body.classList.add('no-scroll');

// 记录滚动位置
const scrollTop = window.scrollY;

// iOS 特殊处理(防止页面跳回顶部)
if (isIOS()) {
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
}
}

// 隐藏浮层时
function hideOverlay() {
document.body.classList.remove('no-scroll');

if (isIOS()) {
const scrollTop = parseInt(document.body.style.top);
document.body.style.position = '';
document.body.style.top = '';
window.scrollTo(0, -scrollTop);
}
}

// 判断是否为 iOS
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
/* CSS 样式 */
.no-scroll {
overflow: hidden;
}

/* iOS 滚动优化 */
.overlay-content {
-webkit-overflow-scrolling: touch;
}

三、各方案对比

方案优点缺点兼容性
全局禁用滚动实现简单滚动位置丢失,iOS 体验差全兼容
局部滚动控制保留滚动状态,体验好实现复杂,需处理边界情况全兼容
CSS 方案性能最优,代码简洁iOS Safari 兼容性差现代浏览器
混合方案兼顾性能与兼容性需区分平台逻辑全兼容(iOS 优化)

四、常见问题与优化

  1. iOS 滚动卡顿
    给可滚动元素添加 -webkit-overflow-scrolling: touch 启用硬件加速。

  2. 滚动位置丢失
    使用 position: fixedtop 属性保存滚动位置(如方案四)。

  3. 模态框嵌套
    维护滚动状态栈,记录每个模态框的滚动位置。

  4. 性能优化
    使用 requestAnimationFrame 优化滚动事件处理,减少重排。

五、总结:如何选择方案?

  • 简单场景:优先使用 CSS 方案touch-action),配合 JS 处理 iOS 兼容。
  • 复杂场景:使用 混合方案,结合 CSS 和 JS 精细化控制。
  • 性能敏感场景:使用 局部滚动控制,避免全局禁用滚动。

通过合理选择方案,可彻底解决滑动穿透问题,同时保证良好的用户体验和兼容性。