问题

对一个标签进行fixed定位时候并设置overflow:scroll时,当该元素可以滚动到边界时候,滚动行为会穿透背后元素直至body均会随之滚动

原因分析

当触及页面顶部或者底部时(或者是其他可滚动区域),移动端浏览器倾向于提供一种“触底”效果,甚至进行页面刷新。你可能也发现了,当对话框中含有可滚动内容时,一旦滚动至对话框的边界,对话框下方的页面内容也开始滚动了——这被称为“滚动链”,这是一种浏览器行为。

示例代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
    .div {
        height: 400px;
        background: red;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }

    .div:nth-child(2) {
        background: green;
    }

    .div:nth-child(3) {
        background: blue;
    }

    .div:nth-child(4) {
        background: yellow;
    }

    .fixed {
        position: fixed;
        right: 0;
        z-index: 10;
        left: 0;
        bottom: 0;
        height: 300px;
        overflow: scroll;
        background: #fff;
    }

    .bg {
        position: fixed;
        z-index: 0;
        right: 0;
        top: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(0, 0, 0, 0.5);
    }
    </style>
</head>

<body>
    <div class="div">1</div>
    <div class="div">2</div>
    <div class="div">3</div>
    <div class="div">4</div>
    <div class="bg"></div>
    <div class="fixed">
        <div style="height:800px;">test</div>
    </div>
</body>

</html>

解决办法一(推荐)

利用css属性overscroll-behavior

.fixed {
  overflow: auto;
  overscroll-behavior: contain; /* 阻止外部滚动 */
}

解决办法二

js边界判断

const scrollElement = document.querySelector('.fixed');

scrollElement.addEventListener('touchstart', (event) => {
  const scrollTop = scrollElement.scrollTop;
  const scrollHeight = scrollElement.scrollHeight;
  const offsetHeight = scrollElement.offsetHeight;
  const atTop = scrollTop === 0;
  const atBottom = scrollTop + offsetHeight === scrollHeight;

  if (atTop) {
    scrollElement.scrollTop = 1; // 防止触发背景滚动
  } else if (atBottom) {
    scrollElement.scrollTop = scrollTop - 1; // 防止触发背景滚动
  }
});

scrollElement.addEventListener('touchmove', (event) => {
  const scrollTop = scrollElement.scrollTop;
  const scrollHeight = scrollElement.scrollHeight;
  const offsetHeight = scrollElement.offsetHeight;

  if (scrollTop === 0 && event.touches[0].clientY > 0) {
    event.preventDefault(); // 防止向上滚动
  } else if (scrollTop + offsetHeight === scrollHeight && event.touches[0].clientY < 0) {
    event.preventDefault(); // 防止向下滚动
  }
}, { passive: false });

解决办法三

使用成熟的库如 body-scroll-lock,可以更优雅地解决滚动穿透问题。

import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';

const scrollElement = document.querySelector('.scroll-container');

// 禁用背景滚动
disableBodyScroll(scrollElement);

// 恢复背景滚动
enableBodyScroll(scrollElement);