研究了2天的平滑滚动,后面又结合锚点的实现,感觉收获很多,因此写下记录来整理一下。
最常见的需求是一个较长的页面的右下角可能有一个按钮,点击它就能回到顶部。这一般都是用锚点实现的,但是原生锚点的缺点是直接跳转,过于生硬。
因此我们需要一种平滑滚动的实现。
注:使用codepen时,codepen的浏览器环境就是你浏览器的环境,因此以下示例如果不能跑通,请检查你的浏览器环境。
使用css实现
对于较新的浏览器,终于等到了css的原生实现,不再需要js来实现平滑滚动。
css属性
scroll-behavior
对由
navigation
或
CSSOM scrolling APIs
触发的滚动生效。当其值为
smooth
时,点击对应锚点的a标签,就会出现平滑的匀速滚动到相应的锚点位置。因为锚点行为是
CSSOM scrolling APIs
触发
MDN上详细的了解
scroll-behavior
想让其在整个html中生效,我们只需要
css
这么写就ok了:
html {
scroll-behavior: smooth;
复制代码
注:在body上使用这个属性是无效的,详见mdn
codePen上示例
chrome从chrome 61(2017.9)开始支持这个属性,因此可见这个属性是比较新的,只有较新版本的chrome,Firefox,Opera支持。而Safari,Edge,IE的任何版本都不支持。
使用window.scrollTo()
window.scrollTo有两种用法
window.scrollTo(x-coord, y-coord)
window.scrollTo(options)
复制代码
第一种用法就和普通锚点一样,立刻滚动到相应的位置。只有第二种用法能实现平滑滚动,其中传入的
options
里的
behavior
:
smooth
,可以实现匀速的平滑滚动
window.scrollTo({
top: 100,
left: 100,
behavior: 'smooth'
复制代码
codePen上示例
遗憾的是第二种用法兼容性也比较差。
只有较新版本的chrome,Firefox,Opera支持。而Safari,Edge,IE的任何版本都不支持。
使用requestAnimationFrame实现
当以上2种原生方法都不能使用时,我们就需要自己手动利用js实现。
requestAnimationFrame
是浏览器提供用来绘制动画一个api。它会在浏览器的每一帧Layout,Paint之前执行,就是常说的回流和重绘之前执行,这样可以很方便的执行一段更改样式的代码,然后浏览器就能马上回流,重绘,生成一帧。
使用requestAnimationFrame的优点
目前来说,使用requestAnimationFrame实现是比较好的一种做法
使用requestAnimationFrame实现有哪些好处?
相对于过去使用setInterval实现,requestAnimationFrame性能要好。requestAnimationFrame保证了每次改动样式后再进行回流重绘,setInterval可能使浏览器作出无效的回流和重绘。requestAnimationFrame在每一帧的生命周期都会触发,会使动画更加流畅,而setInterval不能保证每一帧都能触发。
requestAnimationFrame的兼容性是非常好的,可以一直向下兼容到IE10。对于IE9及其以下,可以降级使用setTimeout或者setInterval实现requestAnimationFrame的polyfill。
自己写就可以实现各种不同的滚动速度了,可以实现线性速度,也可以实现先加速后减速的效果。
raf实现一个匀速滚动
下面是一个尝试使用requestAnimationFrame实现的示例。
结合这个示例,顺便介绍一下requestAnimationFrame的用法
首先可以通过
document.documentElement.scrollTop
获得当前页面滚动条的位置。
document.documentElement.scrollTop代表滚动条到页面顶端的距离,当document.documentElement.scrollTop === 0时,说明滚动条在页面的最顶端,
document.documentElement.scrollTop
document.documentElement.scrollTop = 0
复制代码
接下来我们来使用requestAnimationFrame来实现匀速滚动最简单的示例:
<div id="button">回到顶部</div>
复制代码
let button = document.getElementById('button')
button.addEventListener('click',function(){
let cb = ()=>{
if(document.documentElement.scrollTop===0){
return;
} else {
let speed = 40
document.documentElement.scrollTop -= speed
requestAnimationFrame(cb)
requestAnimationFrame(cb)
复制代码
requestAnimationFrame接受一个callback函数,会在浏览器的下一帧中的某个时间执行这个callback函数。注意的是只有下一帧,因此需要在callback函数中进行条件判断,当不满足条件时再次将callback函数传入raf
你可以在codePen查看这个示例:
codePen
实例中实现了匀速滚动,并且可以根据变量来控制滚动的速度。
接下来考虑一个功能,点击按钮,不论在滚动条哪个位置,都能在大概2秒的时间回到页面的顶部,这个应该怎么实现?而且匀速运动的动画显得有些生硬,能否实现先加速后减速的功能?
raf实现变速滚动到顶端,并控制滚动的时间为2秒
1.如何实现2秒内滚动到位?
一般而言,目前最广泛的硬件设备刷新率是60帧,浏览器在硬件2帧之间执行回流重绘是没有意义的。因此浏览器一帧维持的时间是16.7ms,可以大致认为浏览器一帧的时间是1/60s,连续调用120次
raf(cb)
大致就是2秒钟。
2.如何实现变速滚动?
滚动的距离是一定的,令其为
distance
,我们需要在120帧里让
document.documentElement.scrollTop
的值减为0。
const distance = document.documentElement.scrollTop
复制代码
这个动画过程中,速度就是一帧里scrollTop减掉的数值。理解这个我们就能用一个等差数列来实现一个变速滚动。
第1帧 document.documentElement.scrollTop - x * 1
第2帧 document.documentElement.scrollTop - x * 2
第3帧 document.documentElement.scrollTop - x * 3
第60帧 document.documentElement.scrollTop - x * 60
第61帧 document.documentElement.scrollTop - x * 60
第62帧 document.documentElement.scrollTop - x * 59
第63帧 document.documentElement.scrollTop - x * 58
第120帧 document.documentElement.scrollTop - x * 1
复制代码
所以
let x = distance / (1+2+3···+60+60+59+···+2+1)
let x = distance / 60 / 61
复制代码
下面完整的js代码,同时统计了滚动的时间。
let button = document.getElementById('button')
let dateBegin;
let dateEnd;
button.addEventListener('click',function(){
dateBegin = Date.now()
let current = document.documentElement.scrollTop
new Promise((resolve,reject)=>{
let unit = current/60/61
let index = 0
let cb = ()=>{
if(document.documentElement.scrollTop===0){
dateEnd = Date.now()
resolve([dateBegin,dateEnd])
} else{
index++
if(index<=60){
current-=index*unit
} else if(index>60){
current-=(121-index)*unit
document.documentElement.scrollTop = current
requestAnimationFrame(cb)
requestAnimationFrame(cb)
}).then((data)=>{
console.log(data[1] - data[0],' ms')
复制代码
codePen查看
codePen
控制台输出在1900ms左右,并且可以看出是滚动是先加速,后减速的。
但是这样实现时间控制,是依赖于设备是保证60帧刷新的,如果当前页面有大量动画渲染或者复杂的js计算,导致浏览器帧数不能保证60帧运行呢?
这里其实对raf的使用是比较粗略的,比如raf对于传入的cb会提供一个时间参数,可以精确到5μs。利用这个可以精确的控制滚动的时间。还可以增加拓展功能,可以调用
cancelAnimationFrame
来取消raf的执行,从而在滚动过程中终止滚动。
下篇会分析
smooth-scroll
这个库的源码,这个库的实现很精彩,而且支持的变速滚动方式很多,并且能够很好的自定义。在vue的API文档中,点击侧栏,文档会平滑滚到相应的api的位置,就是用这个库实现的。这里暂时放一下。
最终手段:使用setInterval或者setTimeout实现
这个用法可以说是兼容性最高的方法了。当浏览器不支持raf时,我们可以使用raf的polyfill,raf的polyfill就是用setTimeout实现的。这个和raf的写法是差不多的,就不赘述了。当需要兼容不支持raf的浏览器时,最好的办法是使用raf的polyfill,这样既保证了优雅降级,也保证了在现代浏览器上能更好的工作。