我们可以通过传递 behavior: smooth 平滑滚动到给定元素:

ele.scrollIntoView({ behavior: 'smooth' })

或将 CSS 属性滚动行为应用于目标元素:

scroll-behavior: smooth;

这两种方法我们之前介绍过,详细内容请查看滚动到页面顶部的多种实现

IE 和 Safari 中不支持这两种方法,并且不允许自定义动画。

这篇文章介绍了一个平滑滚动的实现,它还允许我们自定义动画效果和持续时间。我们将在一个流行的用例中演示,用户可以通过单击相关的导航按钮在各个部分之间切换。

导航由一些 a 标签组成:

<a href="#section-1" class="trigger"></a>
<a href="#section-2" class="trigger"></a>
...

<div id="section-1">...</div>
<div id="section-2">...</div>

单击该链接将滚动页面至可由 href 属性确定的特定元素。

const triggers = [].slice.call(document.querySelectorAll('.trigger'))
triggers.forEach(function (ele) {
  ele.addEventListener('click', clickHandler)
})

clickHandler 方法处理导航元素的单击事件。它根据 href 属性确定目标节点。请注意,我们将自己滚动到目标部分,因此默认操作将被忽略:

const clickHandler = function (e) {
  // 阻止默认操作
  e.preventDefault()

  // 获取 href 属性
  const href = e.target.getAttribute('href')
  const id = href.substr(1)
  const target = document.getElementById(id)

  scrollToTarget(target)
}

如果您还没有看到 scrollToTarget 方法,请不要担心。顾名思义,该方法将滚动页面到给定的目标。

滚动到给定目标

这是这篇文章的主要部分。要滚动到给定的点,我们可以使用 window.scrollTo(0,y),其中 y 表示从页面顶部到目标的距离。

也可以设置 behavior: 'smooth',但 IE 和 Safari 中不支持该选项。

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

我们要做的是在给定的持续时间内从起点移动到终点。

起点是当前 y 轴偏移 window.pageYOffset

终点是目标的顶部距离。它可以作为 target.getBoundingClientRect().top 进行检索

持续时间为毫秒数。您可以将其更改为可配置选项,但在本文中,它被设置为 800。

const duration = 800;

const scrollToTarget = function (target) {
  const top = target.getBoundingClientRect().top
  const startPos = window.pageYOffset
  const diff = top

  let startTime = null
  let requestId

  const loop = function (currentTime) {
    if (!startTime) {
      startTime = currentTime
    }

    // 已用时间(毫秒)
    const time = currentTime - startTime

    const percent = Math.min(time / duration, 1)
    window.scrollTo(0, startPos + diff * percent)

    if (time < duration) {
      // 继续前进
      requestId = window.requestAnimationFrame(loop)
    } else {
      window.cancelAnimationFrame(requestId)
    }
  }
  requestId = window.requestAnimationFrame(loop)
}

如您所见,我们告诉浏览器在下一次绘制之前执行循环函数。第一次,startTime 将被初始化为当前时间戳(currentTime)。

然后我们计算已经过去了多少毫秒:

const time = currentTime - startTime

根据经过的时间和持续时间,很容易计算我们移动的百分比数,并滚动到该位置:

// percent 在 0 到 1 之间
const percent = Math.min(time / duration, 1)
window.scrollTo(0, startPos + diff * percent)

最后,如果还有剩余的时间,我们继续循环。否则,我们将取消最后一个请求:

if (time < duration) {
  requestId = window.requestAnimationFrame(loop)
} else {
  window.cancelAnimationFrame(requestId)
}

良好做法

当我们需要在给定的持续时间内在给定的点之间移动时,通常会想到使用 setTimeout()setInterval()。但建议使用 requestAnimationFrame,它可以提供更好的动画性能。

自定义动画

目前,我们平均每毫秒移动到目标。我们每毫秒移动相同的距离。

如果需要,可以使用其他缓和功能替换当前的线性运动。看看这个网站,想象一下每种放松是如何产生不同的动画的。

下面的代码使用 easeInQuad 动画:

const easeInQuad = function(t) {
  return t * t
}

const loop = function(currentTime) {
  //...
  const percent = Math.min(time / duration, 1)
  window.scrollTo(0, startPos + diff * easeInQuad(percent))
})

查看效果

https://htmldom.dev/scroll-to-an-element-smoothly/