前端 · 9月 7, 2021 0

浏览器 Scroll 平滑滚动

除了用户主动触发的滚动,我们经常需要代码触发滚动。然而浏览器提供的scroll API,并不是完全精确,存在比较多的兼容性。今天我们就来探讨一下,scroll 滚动API 及其兼容性。

几个重要的对象和类

window

window 对象表示浏览器对象模型,即浏览器提供的扩展API能力。大写的Window是构造函数,小写的window是它实例化后的对象。

scrollXscrollYpageXOffsetpageYOffset 是它的固有属性。这会返回垂直/水平上的滚动距离。

返回而非设置,即只读。

它实现了scrollscrollByscrollToscrollIntoView方法。它滚动的是整个文档,内联可滚动元素不会发生滚动。

document

document对象表示文档对象模型,是浏览器提供给js操作文档元素的API。 大写的Document是构造函数,小写的document是它实例化后的对象。

document 对象实现了 HTMLDocument 类。

documentElementbody是它的固有属性。

documentElement是只读的,返回文档根元素,在html文档中就是<html>body是可读可写的。

Element

Element 是一个通用性非常强的基类,所有 Document 对象下的对象都继承自它。我们使用元素选择器获取元素时,返回的对象,继承自HTMLElement,而HTMLElement继承自Element

scrollTopscrollHeightscrollLeftscrollWidth 是它的固有属性。

scrollTopscrollLeft可读写;scrollHeightscrollWidth只读。

同时,它也实现了scrollscrollByscrollToscrollIntoView方法,控制指定元素的滚动,前提是可以滚动。

获取滚动距离

获取滚动距离的方式比较多。

想要获取根元素的滚动距离,根据上面的介绍,比较简单:

window.pageYOffset || document.documentElement.scrollTop

pageYOffset 和 scrollY 时等价的,建议使用pageYOffset。

非根元素的滚动距离,根据Element元素类的属性:

document.querySelector('p').scrollTop

确保你选中了滚动容器。

所以,很多框架里面是这么获取滚动距离的:

element.pageYOffset || elememt.scrollTop

利用Element没有pageYOffset属性,可以返回根元素或非根元素的滚动距离。

你会发现,无论何种方法,返回的滚动距离总是整数

设置滚动距离

设置滚动距离只需要从第一个小节中,使用可写的属性即可达到目的。

如果你想设置根元素,有两种途径,设置document或者window的滚动。设置document的滚动:

document.documentElement.scrollTop = 100

或者:

document.documentElement.scrollTo(0, 100)

这样即可让页面产生滚动。

那么问题来了,window对象上没有可写的滚动属性,怎么办?只能用方法来实现:

window.scrollTo(0, 100)

以上两种方法产生的效果时一致的!

非根元素的滚动距离设置,同document的设置方法:

document.querySelector('p').scrollTop = 100

或者:

document.querySelector('p').scrollTo(0, 100)

scrollTo 和 scroll 是等同的,scrollTo 是绝对滚动,即滚动至。scrollBy 是相对滚动,即滚动一定距离。

你会发现,无论何种方法,设置滚动距离时,生效的总是设置值的整数部分

平滑滚动

上面的方法都是突兀的滚动,用户体验比较差,更多时候我们需要平滑的滚动。

浏览器API

ScrollToOptions用于指定一个元素应该滚动到哪里,以及滚动是否应该平滑。

作用范围:

  1. Window.scroll()
  2. Window.scrollBy()
  3. Window.scrollTo()
  4. Element.scroll()
  5. Element.scrollBy()
  6. Element.scrollTo()

ScrollToOptions.behavior 指定滚动是否应该平滑进行,还是立即跳到指定位置。例如:

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

这会平滑的滚动至100,而非突然滚动。

然而,该参数在safair上并不支持。

为了实现兼容性的平滑滚动,你需要自己实现这个平滑的滚动过程。幸好,该参数有polyfill: smoothscroll-polyfill。引入之后,有关滚动的API,都会支持平滑参数。

smoothscroll-polyfill 的原理

判断是否支持 behavior

'scrollBehavior' in document.documentElement.style

巧妙的使用scrollBehavior的样式有无,来判断。实际上,很多API的兼容性,都可以用类似的方法。

覆写原型

覆写了以下原型:

  1. window.scroll()window.scrollTo()
  2. window.scrollBy()
  3. Element.prototype.scrollElement.prototype.scrollTo
  4. Element.prototype.scrollBy
  5. Element.prototype.scrollIntoView

平滑滚动

在不支持的情况下,写一个自定义的平滑滚动的实现smoothScroll。它的原理是:

计算出待滚动的距离distance,并给出一个合理的滚动时间t,即要在t时间内,平滑的滚动完distance。平滑的实现是通过每次滚动一点距离,直到滚动完成。每次滚动的距离是通过计算得出的,计算函数采用的是缓动函数:

y = (1-cos(πx))/2

定义域和值域都是[0, 1]。

它的函数曲线图如下:

通过曲线的斜率,我们可以看出,动画是缓慢启动,缓慢结束。有点像easeInOut淡入淡出。

每次滚动是通过递归step函数实现的,递归的触发条件是,滚动的距离未达到目标值。其函数源码如下:

    function step(context) {
      var time = now();
      var value;
      var currentX;
      var currentY;
      var elapsed = (time - context.startTime) / SCROLL_TIME;

      // avoid elapsed times higher than one
      elapsed = elapsed > 1 ? 1 : elapsed;

      // apply easing to elapsed time
      value = ease(elapsed);

      currentX = context.startX + (context.x - context.startX) * value;
      currentY = context.startY + (context.y - context.startY) * value;

      context.method.call(context.scrollable, currentX, currentY);

      // scroll more if we have not reached our destination
      if (currentX !== context.x || currentY !== context.y) {
        w.requestAnimationFrame(step.bind(w, context));
      }
    }

其中 ease (elapsed ),是根据已消耗的时间比例,计算缓动函数值,然后再乘以总滚动距离,得出此时刻应该滚动的距离。

其滚动事件通过requestAnimationFrame触发,不影响UI渲染。

结束语

由于滚动方法会舍去小数点的部分,滚动像素会出现误差,误差并不大,绝大部分情况无影响。smoothscroll-polyfill 能解决大部分场景。

冀ICP备19028007号