滚动交互体系

9.1 滚动交互体系

滚动交互是 Web 页面最常见的交互场景之一,覆盖资源加载、列表浏览、布局联动等多个业务场景。传统滚动交互依赖 scroll 事件配合位置计算实现,存在高频触发、性能开销大、实现繁琐等问题。结合 IntersectionObserver、CSS 粘性定位等原生能力,可以实现高性能、低代码量的滚动交互方案,同时配套滚动穿透等边界问题的处理,构建完整的滚动交互体系。


9.1.1 基于 IntersectionObserver 的图片懒加载

图片懒加载是最经典的滚动性能优化手段,核心思路是仅当图片进入可视区域时才加载真实图片,减少首屏加载流量与请求数,提升页面加载速度。

一、传统方案的痛点

早期基于 scroll 事件 + getBoundingClientRect 实现的懒加载,存在明显缺陷:

  • scroll 事件高频触发,即使加节流也存在持续的性能开销
  • 每次都需要手动计算元素位置,频繁触发强制同步布局
  • 多图片场景逻辑繁琐,维护成本高

IntersectionObserver 异步监听元素交叉状态,不阻塞主线程,是当前懒加载的标准实现方案。

二、实现原理
  1. 图片默认不设置 src 属性,真实地址存放在 data-src 自定义属性中,占位使用统一默认图或空白背景
  2. 创建 IntersectionObserver 实例,批量监听所有懒加载图片
  3. 图片进入可视区域时,将 data-src 赋值给 src 属性,触发真实图片加载
  4. 加载完成后停止监听该图片,避免重复处理
三、完整实现代码

HTML 结构:

html 复制代码
<img class="lazy-img" data-src="[https://example.com/real-img.jpg](https://example.com/real-img.jpg)" alt="懒加载图片">

JS 实现:

javascript 复制代码
// 创建交叉观察者

const lazyObserver = new IntersectionObserver((entries) => {

  entries.forEach(entry => {

    // 元素进入可视区域

    if (entry.isIntersecting) {

      const img = entry.target;

      const realSrc = img.dataset.src;



      if (realSrc) {

        // 替换真实图片地址

        img.src = realSrc;

        // 加载完成后添加状态标识

        img.onload = () => {

          img.classList.add('loaded');

        };

        // 停止监听该元素

        lazyObserver.unobserve(img);

      }

    }

  });

}, {

  rootMargin: '200px 0', // 提前200px预加载,减少用户等待

  threshold: 0

});



// 批量监听所有懒加载图片

document.querySelectorAll('.lazy-img').forEach(img => {

  lazyObserver.observe(img);

});
四、优化细节
  1. 提前预加载 :通过 rootMargin 设置正向边距,图片进入视口前提前加载,避免用户看到加载过程。
  2. 占位与过渡:默认设置占位图或统一背景色,加载完成后加淡入过渡,避免图片加载闪烁。
  3. 错误兜底 :监听 onerror 事件,加载失败时替换为默认失败图,避免裂图。
  4. 动态新增兼容 :动态渲染的图片,插入 DOM 后手动调用 observe 即可无缝接入懒加载。
  5. 降级兼容:低版本浏览器不支持 IntersectionObserver 时,可降级为一次性加载所有图片或引入 polyfill。

9.1.2 无限滚动实现方案

无限滚动(下拉加载更多)是长列表、信息流场景的标准交互模式,用户滚动到列表底部时自动加载下一页数据,无需分页跳转,浏览体验更流畅。

一、核心设计思路

在列表底部放置一个「哨兵元素」(sentinel),通过 IntersectionObserver 监听该哨兵元素的可见性:

  • 哨兵进入视口 → 说明滚动到底部 → 触发下一页数据加载
  • 加载过程中加状态锁,防止重复触发请求
  • 所有数据加载完毕后,停止监听,显示「没有更多数据」
二、完整实现

HTML 结构:

html 复制代码
<div class="list-container">

  <div class="list-item">列表项1</div>

  <div class="list-item">列表项2</div>

  <!-- 底部哨兵元素 -->

  <div class="load-more-sentinel"></div>

  <div class="no-more-tip" style="display: none;">没有更多数据了</div>

</div>

JS 实现:

javascript 复制代码
let page = 1;

let loading = false; // 加载锁,防止重复请求

let hasMore = true;

const pageSize = 20;



const sentinel = document.querySelector('.load-more-sentinel');



const infiniteObserver = new IntersectionObserver(async (entries) => {

  const entry = entries[0];

  // 哨兵进入视口、有更多数据、不在加载中,三者同时满足才触发

  if (entry.isIntersecting && hasMore && !loading) {

    loading = true;

    try {

      const res = await fetchListData({ page, pageSize });

      // 渲染列表数据

      renderList(res.list);

     

      if (res.list.length < pageSize) {

        // 数据不足一页,说明已加载全部

        hasMore = false;

        document.querySelector('.no-more-tip').style.display = 'block';

        infiniteObserver.unobserve(sentinel);

      } else {

        page++;

      }

    } catch (err) {

      console.error('加载失败', err);

    } finally {

      loading = false;

    }

  }

}, {

  rootMargin: '100px 0' // 提前100px触发,减少用户等待

});



infiniteObserver.observe(sentinel);
三、注意事项
  1. 加载锁机制 :必须设置 loading 状态锁,避免网络较慢时滚动触发多次重复请求。
  2. 边界处理:首屏数据不足一屏时,自动加载下一页直到填满屏幕或无更多数据。
  3. 加载状态反馈:加载过程中显示加载动画,给用户明确的等待预期。
  4. 性能对比:相比传统 scroll 监听 + 位置计算,IntersectionObserver 方案无高频触发、无强制布局计算,性能更优,逻辑更简洁。

9.1.3 吸顶与吸底效果

吸顶(元素滚动到顶部后固定在视口顶部)、吸底(元素滚动到底部后固定在视口底部)是最常见的滚动布局交互,分为 CSS 原生与 JS 两种实现方案。

一、CSS 方案:position: sticky(首选方案)

CSS 粘性定位是原生实现吸顶吸底的标准方案,浏览器底层优化,性能最优,代码极简。

基础用法
javascript 复制代码
/* 吸顶:滚动到顶部后固定在 top:0 位置 */

.sticky-header {

  position: sticky;

  top: 0;

  z-index: 10;

}



/* 吸底:滚动到底部后固定在 bottom:0 位置 */

.sticky-footer {

  position: sticky;

  bottom: 0;

}
特点与限制

|---------|-------------------------------------------------------------|
| 维度 | 说明 |
| 优势 | 零 JS 代码、原生高性能、无页面跳动问题 |
| 限制1 | 父元素不能设置 overflow: hidden / overflow: auto,否则 sticky 会失效 |
| 限制2 | 元素仅在父元素范围内生效,父元素整体滚出视口后,元素会一起滚走 |
| 兼容性 | 现代浏览器全支持,IE 不兼容 |

二、JS 方案:scroll 监听 + fixed 定位

复杂场景(如吸顶过程中有样式渐变、需要精确控制时机)下,使用 JS 手动实现。

实现原理
  1. 提前记录元素初始位置,使用占位元素保持高度,避免元素脱离文档流导致页面跳动
  2. 监听 scroll 事件,当滚动距离超过元素顶部偏移量时,切换为 position: fixed
  3. 滚动回原位时,恢复默认定位,隐藏占位元素
基础实现
javascript 复制代码
const header = document.querySelector('.sticky-header');

// 创建占位元素,防止高度丢失导致页面跳动

const placeholder = document.createElement('div');

placeholder.style.height = header.offsetHeight + 'px';

placeholder.style.display = 'none';

header.parentNode.insertBefore(placeholder, header);



const headerTop = header.offsetTop;



// 节流优化滚动事件

const handleScroll = throttle(() => {

  const scrollTop = window.scrollY;

  if (scrollTop >= headerTop) {

    header.style.position = 'fixed';

    header.style.top = '0';

    placeholder.style.display = 'block';

  } else {

    header.style.position = 'static';

    placeholder.style.display = 'none';

  }

}, 16);



window.addEventListener('scroll', handleScroll);
三、方案选型建议
  • 常规吸顶吸底优先使用 position: sticky,性能最好、实现最简单
  • 需要动态样式变化、复杂交互逻辑的场景,再使用 JS 方案,配合节流优化 scroll 事件性能

9.1.4 滚动穿透处理

滚动穿透是弹窗、抽屉等浮层场景的常见问题:当浮层内部滚动到底部/顶部后,继续滚动会带动背后的底层页面一起滚动,破坏交互体验。

一、方案一:CSS overscroll-behavior(现代浏览器首选)

原生 CSS 属性,专门用于控制滚动边界行为,代码最简单,性能最优。

javascript 复制代码
/* 浮层可滚动内容容器 */

.modal-content {

  overflow-y: auto;

  overscroll-behavior: contain;

}
  • 原理:设置为 contain 后,容器滚动到边界时不会向父元素传递滚动行为,彻底隔离滚动
  • 优势:一行 CSS 解决,无副作用
  • 局限:低版本浏览器不支持,部分移动端 webview 兼容性略差
二、方案二:body 固定定位(PC 端全屏弹窗常用)

弹窗打开时,给 body 设置固定定位,记录当前滚动位置;关闭弹窗后恢复样式与滚动位置,完全禁止底层滚动。

实现代码
javascript 复制代码
let savedScrollTop = 0;



// 打开弹窗时

function openModal() {

  savedScrollTop = window.scrollY;

  document.body.style.position = 'fixed';

  document.body.style.top = `-${savedScrollTop}px`;

  document.body.style.width = '100%';

}



// 关闭弹窗时

function closeModal() {

  document.body.style.position = '';

  document.body.style.top = '';

  document.body.style.width = '';

  // 恢复页面滚动位置

  window.scrollTo(0, savedScrollTop);

}
  • 特点:完全禁止底层页面滚动,彻底解决穿透问题
  • 注意:fixed 定位会导致页面默认回到顶部,必须记录并恢复滚动位置;适合全屏遮罩弹窗场景
三、方案三:JS 事件阻止(移动端 touch 场景)

监听浮层的 touchmove 事件,判断当前滚动位置与滑动方向,在边界处阻止默认行为,避免滚动穿透。

实现逻辑
  1. 触摸开始时记录初始纵坐标
  2. 触摸移动时,判断滚动方向与当前滚动位置
  3. 若已在顶部且继续下拉,或已在底部且继续上拉,则阻止事件
  4. 中间滚动区域正常响应,不阻止事件
javascript 复制代码
const content = document.querySelector('.modal-content');

let startY = 0;



content.addEventListener('touchstart', (e) => {

  startY = e.touches[0].clientY;

});



content.addEventListener('touchmove', (e) => {

  const currentY = e.touches[0].clientY;

  const deltaY = currentY - startY;

  const scrollTop = content.scrollTop;

  const scrollHeight = content.scrollHeight;

  const clientHeight = content.clientHeight;



  // 顶部且继续下拉,阻止穿透

  if (scrollTop <= 0 && deltaY > 0) {

    e.preventDefault();

  }

  // 底部且继续上拉,阻止穿透

  if (scrollTop + clientHeight >= scrollHeight && deltaY < 0) {

    e.preventDefault();

  }

}, { passive: false });
  • 特点:精准控制,仅边界处阻止,内部正常滚动
  • 注意:事件必须设置 passive: false 才能调用 preventDefault
四、方案选型
  • 现代浏览器优先使用 overscroll-behavior: contain
  • PC 端全屏弹窗可使用 body 固定方案
  • 移动端复杂交互场景使用 JS 事件阻止方案
相关推荐
时空节拍AI数字人1 小时前
博物馆数字人讲解系统设计:知识库 schema、展项联动协议与时序控制
java·服务器·前端·人工智能·ai·语音识别
吴建旭 智宅焕1 小时前
智能家居全国交付中前端工具与后端基础设施的系统集成架构:报价入口与交付兜底的闭环设计
前端·架构·智能家居
开开心心就好1 小时前
电脑小说阅读器,支持朗读和自定义皮肤
服务器·开发语言·javascript·算法·pdf·c#·音视频
画中有画2 小时前
论可观测性架构在分布式系统中的应用
1024程序员节
夏天要喝冰可乐2 小时前
Codex 写 Three.js 大屏实录:800 行代码,和一条画反的弧线
前端·aigc
欣欣之王来了2 小时前
v-bind语法糖::属性名的简写方式
前端·学习·架构·项目·vue3教程
2601_954005622 小时前
# ricon SVG 编辑器:工业图元的“军火工厂“
前端·物联网·数学建模·架构·编辑器·制造
codeniu2 小时前
一个人,5天,从想法到上线:我用 TRAE 跑通全链路开发的完整实录
前端
惊讶的猫2 小时前
慢查询和排查方式(查询日志 + 分析工具 + EXPLAIN)
mysql·1024程序员节