Web浏览器滚动方案一览| scrollTo、scrollBy、scrollIntoView、使用rAF等

ByteLuminaPro
• 阅读 1614

在Web开发中,实现流畅的滚动效果对于提升用户体验至关重要。为了实现这一目标,开发人员可以利用一系列的滚动方案。其中,请求动画帧(requestAnimationFrame,简称rAF)是一种常用的技术。rAF通过优化动画效果的渲染,可以避免卡顿和过度绘制的问题。此外,还有其他滚动方案如CSS动画、滚动事件监听等等,开发人员可以根据具体需求选择合适的方案。通过合理选择和应用这些滚动方案,我们可以提供更加流畅和优化的用户体验。

Window 大小与文档大小

要获取窗口大小和文档大小,我们可以使用JavaScript编程语言。通过使用window对象的innerWidth和innerHeight属性,我们可以获取窗口的宽度和高度。而要获取文档的大小,我们可以使用document对象的clientWidth和clientHeight属性。这些属性将返回以像素为单位的值,从而使我们能够准确地确定窗口和文档的尺寸。通过使用这些属性,我们可以对网页进行响应式设计,并确保其在不同设备上的显示效果良好。

在 Chrome/Safari/Opera 中,如果没有滚动条,documentElement.scrollHeight 甚至可能小于 documentElement.clientHeight

为了可靠地获得完整的文档高度,我们应该采用以下这些属性的最大值:

let scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

alert('Full document height, with scrolled out part: ' + scrollHeight);

为什么这样?最好不要问。这些不一致来源于远古时代,而不是“聪明”的逻辑。

获取当前滚动

获取文档或DOM元素当前滚动状态是前端开发中很常见的需求。根据标准,我们可以通过元素的scrollLeftscrollTop属性来获取其当前水平和垂直滚动的像素位置。对于整个页面,我们可以使用document.documentElement的这两个属性。但是,需要注意,在旧版的WebKit内核浏览器(如早期版本的Safari)中,这两个属性返回无效值,我们需要使用document.body来取代。

为了兼容所有主流浏览器,一个更好的方式是直接使用window对象的pageXOffsetpageYOffset属性。这两个属性分别返回页面内容区域从文档左上角滚动了多少像素,它们提供了一种跨浏览器兼容的方式来获取当前页面滚动状态。开发人员不必再记住各种浏览器的差异性,只需要调用这两个属性即可简单高效地实现功能。总体来说,获取滚动状态是前端开发中常见的需求之一。我们应该选择最简单高效且兼容所有主流浏览器的方式来实现它,那就是使用window.pageXOffsetwindow.pageYOffset属性。

alert('当前已从顶部滚动:' + window.pageYOffset);
alert('当前已从左侧滚动:' + window.pageXOffset);

这些属性是只读的。

Tips:

我们也可以从 windowscrollXscrollY 属性中获取滚动信息

由于历史原因,存在了这两种属性,但它们是一样的:

  • window.pageXOffsetwindow.scrollX 的别名。
  • window.pageYOffsetwindow.scrollY 的别名。

基于浏览器API的滚动方法

scrollTo

scrollTo 方法用于将页面或元素滚动到指定位置。它接收两个参数,第一个参数是横坐标,第二个参数是纵坐标。

// 将页面滚动到坐标 (0, 100) 
window.scrollTo(0, 100);

// 将元素 elem 滚动到坐标 (0, 0)
elem.scrollTo(0, 0);

scrollTo 方法支持传入behavior,这样可以实现平滑滚动效果。例如:

window.scrollTo({
  top: 100,
  behavior: 'smooth'
});

scrollTo 方法对整个页面和单个元素都起作用,常用于点击某个按钮后滚动到页面指定位置,或者滚动元素内部内容。

scrollBy

scrollBy 方法用于将页面或元素相对当前位置滚动指定的距离。

方法 scrollBy(x,y) 将页面滚动至 相对于当前位置的 (x, y) 位置。例如,scrollBy(0,10) 会将页面向下滚动 10px

// 让元素滚动
elem.scrollBy(300, 300);

使用 options:

elem.scrollBy({
  top: 100,
  left: 100,
  behavior: "smooth",
});

scrollIntoView

为了完整起见,让我们再介绍一种方法:elem.scrollIntoView(top)

// 将元素 elem 滚动到可视区域
elem.scrollIntoView();

elem.scrollIntoView(top) 的调用将滚动页面以使 elem 可见。它有一个参数alignToTop

  • 如果 top=true(默认值),页面滚动,使 elem 出现在窗口顶部。元素的上边缘将与窗口顶部对齐。
  • 如果 top=false,页面滚动,使 elem 出现在窗口底部。元素的底部边缘将与窗口底部对齐。

亦或是接受一个包含以下属性的对象:

  • behavior:定义滚动是立即的还是平滑的动画。可以取值为 smooth(平滑动画)、instant(立即发生)或 auto(由 scroll-behavior 的计算值决定)。
  • block:定义垂直方向的对齐方式,可以取值为 start(顶部对齐)、center(居中对齐)、end(底部对齐)或 nearest(最近对齐)。默认为 start。
  • inline:定义水平方向的对齐方式,可以取值为 start(左对齐)、center(居中对齐)、end(右对齐)或 nearest(最近对齐)。默认为 nearest。
const elem = document.getElementById("box");

elem.scrollIntoView();
elem.scrollIntoView(false);
elem.scrollIntoView({ block: "end" });
elem.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });

实现滚动动画

使滚动动画并兼容非现代浏览器

behavior: "smooth" 等使用behavior参数的Scroll API需要在较高版本浏览器(实际上主要是Safari浏览器版本要求较高):

<img src="https://fs.lwmc.net/uploads/2023/10/1696517179824-202310052246610.webp" alt="image-20231005224618442" style="zoom: 67%;" />

如图所示的window.scrollTo API 中behavior参数的兼容性,所以通常需要补充一下非现代浏览器的方法作为兜底:

  1. 实现基于raf的滚动函数ScrollTo

    /**
     * @description 基于raf的滚动函数
     * @param el 元素
     * @param to 目标滚动位置
     * @param duration 动画时长 ms
     */
    export const rAFScrollTo = (el: HTMLElement, to: number, duration: number) => {
      const start = el.scrollTop
      const change = to - start
      const increment = 1000 / 60
      let currentTime = 0
      let requestId: number | undefined
    
      const cancelRAF = () => {
        if (requestId) {
          cancelAnimationFrame(requestId)
          window.removeEventListener('wheel', cancelRAF)
        }
      }
      // 监听鼠标滚轮
      window.addEventListener('wheel', cancelRAF)
    
      const animateScroll = () => {
        currentTime += increment
        el.scrollTop = easeInOutQuad(currentTime, start, change, duration)
        if (currentTime < duration) {
          requestId = requestAnimationFrame(animateScroll)
        }
      }
    
      const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
        t /= d / 2
        if (t < 1) return (c / 2) * t * t + b
        t--
        return (-c / 2) * (t * (t - 2) - 1) + b
      }
    
      animateScroll()
    }
  2. scrollBy 兼容在非现代浏览器的平滑滚动

    /**
     * @description scrollBy 兼容在非现代浏览器的平滑滚动
     * @param options 传入参数
     * @param options.el 元素
     * @param options.offset 滚动相对距离
     * @param options.duration 动画时长 ms
     * @param options.useRAF 使用RAF动画
     */
    export const scrollBy = ({
      el,
      offset,
      duration = 500,
      useRAF = false,
    }: {
      el: HTMLElement
      offset: number
      duration: number
      useRAF?: boolean
    }) => {
      if (
        typeof window.getComputedStyle(document.body).scrollBehavior ==
          'undefined' ||
        useRAF
      ) {
        // 传统的JS平滑滚动处理代码
        const start = el.scrollTop
        rAFScrollTo(el, start + offset, duration)
      } else {
        el.scrollBy({
          top: offset,
          behavior: 'smooth',
        })
      }
    }
  3. scrollIntoView 兼容在非现代浏览器的平滑滚动

    /**
     * @description scrollIntoView 兼容在非现代浏览器的平滑滚动
     * @param options 传入参数
     * @param options.el 元素
     * @param options.scrollMarginTop 滚动时距离viewport的上边距
     * @param options.duration 动画时长 ms
     * @param options.useRAF 使用RAF动画
     */
    export const scrollIntoView = ({
      el,
      scrollMarginTop = 0,
      duration = 500,
      useRAF = false,
    }: {
      el: HTMLElement
      scrollMarginTop: number
      duration: number
      useRAF?: boolean
    }) => {
      if (
        typeof window.getComputedStyle(document.body).scrollBehavior ==
          'undefined' ||
        useRAF
      ) {
        const { top } = el.getBoundingClientRect()
        const pageYOffset = window.pageYOffset
        const to = top + pageYOffset - scrollMarginTop
        rAFScrollTo(document.documentElement, to, duration)
      } else {
        el.style.scrollMarginTop = scrollMarginTop + 'px'
        el.scrollIntoView({
          behavior: 'smooth',
        })
        el.style.scrollMarginTop = '0px'
      }
    }

禁止滚动

有时候我们需要使文档“不可滚动”。要使文档不可滚动,只需要设置 document.body.style.overflow = "hidden"。该页面将“冻结”在其当前滚动位置上。这个方法的缺点是会使滚动条消失。如果滚动条占用了一些空间,它原本占用的空间就会空出来,那么内容就会“跳”进去以填充它。

这看起来有点奇怪,但是我们可以对比冻结前后的 clientWidth。如果它增加了(滚动条消失后),那么我们可以在 document.body 中滚动条原来的位置处通过添加 padding,来替代滚动条,这样这个问题就解决了。保持了滚动条冻结前后文档内容宽度相同。

亦或是参考这篇文章:css - 如何解决滚动条scrollbar出现造成的页面宽度被挤压的问题? - 个人文章 - SegmentFault 思否

参考

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这