除了用户主动触发的滚动,我们经常需要代码触发滚动。然而浏览器提供的scroll API,并不是完全精确,存在比较多的兼容性。今天我们就来探讨一下,scroll 滚动API 及其兼容性。
几个重要的对象和类
window
window 对象表示浏览器对象模型,即浏览器提供的扩展API能力。大写的Window
是构造函数,小写的window
是它实例化后的对象。
scrollX
、scrollY
、pageXOffset
和 pageYOffset
是它的固有属性。这会返回垂直/水平上的滚动距离。
返回而非设置,即只读。
它实现了scroll
、scrollBy
、scrollTo
和scrollIntoView
方法。它滚动的是整个文档,内联可滚动元素不会发生滚动。
document
document
对象表示文档对象模型,是浏览器提供给js操作文档元素的API。 大写的Document
是构造函数,小写的document
是它实例化后的对象。
document 对象实现了 HTMLDocument
类。
documentElement
和body
是它的固有属性。
documentElement
是只读的,返回文档根元素,在html文档中就是<html>
;body
是可读可写的。
Element
Element 是一个通用性非常强的基类,所有 Document 对象下的对象都继承自它。我们使用元素选择器获取元素时,返回的对象,继承自HTMLElement
,而HTMLElement
继承自Element
。
scrollTop
、scrollHeight
、scrollLeft
和 scrollWidth
是它的固有属性。
scrollTop
和scrollLeft
可读写;scrollHeight
和scrollWidth
只读。
同时,它也实现了scroll
、scrollBy
、scrollTo
和scrollIntoView
方法,控制指定元素的滚动,前提是可以滚动。
获取滚动距离
获取滚动距离的方式比较多。
想要获取根元素的滚动距离,根据上面的介绍,比较简单:
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用于指定一个元素应该滚动到哪里,以及滚动是否应该平滑。
作用范围:
Window.scroll()
Window.scrollBy()
Window.scrollTo()
Element.scroll()
Element.scrollBy()
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的兼容性,都可以用类似的方法。
覆写原型
覆写了以下原型:
window.scroll()
、window.scrollTo()
window.scrollBy()
Element.prototype.scroll
、Element.prototype.scrollTo
Element.prototype.scrollBy
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 能解决大部分场景。