搜索
首页web前端js教程如何在Vanilla JavaScript中实现光滑的滚动

How to Implement Smooth Scrolling in Vanilla JavaScript

核心要点

  • 使用Jump.js库实现原生JavaScript平滑滚动,简化滚动动画,无需外部依赖。
  • 修改Jump.js原始代码,将其从ES6转换为ES5,以确保与不同浏览器的更广泛兼容性。
  • 使用requestAnimationFrame方法进行平滑动画更新,优化性能并提供更流畅的用户体验。
  • 实现自定义JavaScript来拦截默认的页面内链接行为,用平滑滚动动画替换突然跳转。
  • 集成CSS scroll-behavior属性,以支持识别此功能的浏览器中的原生平滑滚动,如果浏览器不支持,则提供JavaScript后备机制。
  • 通过在滚动后将焦点设置到目标元素来确保可访问性,解决键盘导航的潜在问题,并增强所有用户的可用性。

本文由Adrian Sandu、Chris Perry、Jérémy Heleine和Mallory van Achterberg同行评审。感谢所有SitePoint的同行评审者,使SitePoint的内容达到最佳状态!

平滑滚动是一种用户界面模式,它逐步增强了默认的页面内导航体验,在滚动框(视口或可滚动元素)内动画地改变位置,从激活链接的位置到链接URL的哈希片段中指示的目标元素的位置。

这并非什么新鲜事物,多年来一直是一种已知的模式,例如,请查看这篇可追溯到2003年的SitePoint文章!顺便说一句,这篇文章具有历史价值,因为它展示了客户端JavaScript编程,特别是DOM,多年来的变化和发展,允许开发更简便的原生JavaScript解决方案。

在jQuery生态系统中,这种模式有很多实现,可以直接使用jQuery或使用插件实现,但在本文中,我们感兴趣的是纯JavaScript解决方案。具体来说,我们将探索和利用Jump.js库。

在介绍该库及其功能和特性的概述之后,我们将对原始代码进行一些更改以适应我们的需求。在此过程中,我们将复习一些核心的JavaScript语言技能,例如函数和闭包。然后,我们将创建一个HTML页面来测试平滑滚动行为,然后将其实现为自定义脚本。然后将添加对CSS原生平滑滚动的支持(如果可用),最后我们将对浏览器导航历史记录进行一些观察。

这是我们将创建的最终演示:

查看CodePen上的SitePoint (@SitePoint)的Smooth Scrolling笔。

完整的源代码可在GitHub上找到。

Jump.js

Jump.js是用原生ES6 JavaScript编写的,没有任何外部依赖项。它是一个小型实用程序,只有大约42 SLOC,但提供的最小化包的大小约为2.67 KB,因为它必须进行转译。GitHub项目页面上提供了一个演示。

顾名思义,它只提供跳转:滚动条位置从其当前值到目标位置的动画变化,通过提供DOM元素、CSS选择器或正数或负数值形式的距离来指定。这意味着在平滑滚动模式的实现中,我们必须自己执行链接劫持。更多内容请参见以下部分。

请注意,目前仅支持视口的垂直滚动。

我们可以使用一些选项配置跳转,例如持续时间(此参数是必需的)、缓动函数和在动画结束时触发的回调。我们稍后将在演示中看到它们的实际应用。有关完整详细信息,请参见文档。

Jump.js在“现代”浏览器上运行没有问题,包括Internet Explorer 10版或更高版本。同样,请参考文档以了解支持的浏览器完整列表。使用合适的requestAnimationFrame polyfill,它甚至可以在旧版浏览器上运行。

快速了解屏幕背后

在内部,Jump.js源代码使用window对象的requestAnimationFrame方法来安排在滚动动画的每一帧中更新视口垂直位置的位置。此更新是通过将使用缓动函数计算的下一个位置值传递给window.scrollTo方法来实现的。有关完整详细信息,请参见源代码。

一些自定义

在深入研究演示以展示Jump.js的使用之前,我们将对原始代码进行一些细微的更改,但这不会修改其内部工作方式。

源代码是用ES6编写的,需要与JavaScript构建工具一起使用才能进行转译和捆绑模块。对于某些项目来说,这可能有点过分,因此我们将应用一些重构来将代码转换为ES5,以便在任何地方使用。

首先,让我们删除ES6语法和功能。脚本定义了一个ES6类:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

我们可以使用构造函数和一堆原型方法将其转换为ES5“类”,但请注意,我们永远不需要此类的多个实例,因此使用普通对象字面量实现的单例就可以了:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

除了删除类之外,我们还需要进行其他一些更改。requestAnimationFrame的回调用于在每一帧中更新滚动条位置,在原始代码中,它是通过ES6箭头函数调用的,在初始化时预绑定到jump单例。然后,我们将默认缓动函数捆绑在同一个源文件中。最后,我们使用IIFE(立即调用函数表达式)包装了代码,以避免命名空间污染。

现在我们可以应用另一个重构步骤,注意借助嵌套函数和闭包,我们可以只使用函数而不是对象:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

单例现在变成了将被调用以动画滚动的jump函数,loop和end回调变成了嵌套函数,而对象的属性现在变成了局部变量(闭包)。我们不再需要IIFE,因为现在所有代码都安全地包装在一个函数中。

作为最后的重构步骤,为了避免在每次调用loop回调时重复timeStart重置检查,第一次调用requestAnimationFrame()时,我们将向其传递一个匿名函数,该函数在调用loop函数之前重置timerStart变量:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

再次注意,在重构过程中,核心滚动动画代码没有改变。

测试页面

现在我们已经自定义了脚本以适应我们的需求,我们准备组装一个测试演示。在本节中,我们将编写一个使用下一节中介绍的脚本增强平滑滚动的页面。

该页面包含一个包含指向文档中后续部分的页面内链接的内容表(TOC),以及指向TOC的其他链接。我们还将混合一些指向其他页面的外部链接。这是此页面的基本结构:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

在头部,我们将包含一些CSS规则来设置基本的最简布局,而在body标签的末尾,我们将包含两个JavaScript文件:前者是我们重构后的Jump.js版本,后者是我们现在将讨论的脚本。

主脚本

这是将使用我们自定义的Jump.js库版本的动画跳转来增强测试页面滚动体验的脚本。当然,此代码也将用ES5 JavaScript编写。

让我们简要概述一下它应该完成的任务:它必须劫持页面内链接上的点击,禁用浏览器的默认行为(突然跳转到点击链接的href属性的哈希片段中指示的目标元素),并将其替换为对我们的jump()函数的调用。

因此,首先要监控页面内链接上的点击。我们可以通过两种方式做到这一点,使用事件委托或将处理程序附加到每个相关的链接。

事件委托

在第一种方法中,我们将点击侦听器添加到一个元素document.body。这样,页面上任何元素的每个点击事件都将沿着其祖先的分支冒泡到DOM树,直到到达document.body:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

当然,现在在注册的事件侦听器(onClick)中,我们必须检查传入的click事件对象的target,以检查它是否与页面内链接元素相关。这可以通过多种方式完成,因此我们将将其抽象为辅助函数isInPageLink()。我们稍后将看看此函数的机制。

如果传入的点击是在页面内链接上,我们将停止事件冒泡并阻止关联的默认操作。最后,我们调用jump函数,为其提供目标元素的哈希选择器和配置所需动画的参数。

这是事件处理程序:

<code>requestAnimationFrame(function(time) { timeStart = time; loop(time); });

function loop(time) {
    timeElapsed = time - timeStart;

    window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

    if (timeElapsed         requestAnimationFrame(loop)
    else
        end();
}
</code>

单个处理程序

使用第二种方法来监控链接点击,将上面介绍的事件处理程序的稍微修改后的版本附加到每个页面内链接元素,因此没有事件冒泡:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

我们查询所有元素,并使用[].slice()技巧将返回的DOM NodeList转换为JavaScript数组(如果目标浏览器支持,更好的替代方法是使用ES6 Array.from()方法)。然后,我们可以使用数组方法过滤页面内链接,重新使用上面定义的相同辅助函数,最后将侦听器附加到剩余的链接元素。

事件处理程序与之前几乎相同,但当然我们不需要检查点击目标:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

哪种方法最好取决于使用上下文。例如,如果在初始页面加载后可能动态添加新的链接元素,那么我们必须使用事件委托。

现在我们转向isInPageLink()的实现,我们在之前的事件处理程序中使用此辅助函数来抽象页面内链接的测试。正如我们所看到的,此函数接受DOM节点作为参数,并返回一个布尔值以指示该节点是否表示页面内链接元素。仅检查传递的节点是A标签并且设置了哈希片段是不够的,因为链接可能是指向另一个页面,在这种情况下,必须不禁用默认浏览器操作。因此,我们检查属性href中存储的值“减去”哈希片段是否等于页面URL:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

stripHash()是另一个辅助函数,我们也用它在脚本初始化时设置变量pageUrl的值:

<code>requestAnimationFrame(function(time) { timeStart = time; loop(time); });

function loop(time) {
    timeElapsed = time - timeStart;

    window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

    if (timeElapsed         requestAnimationFrame(loop)
    else
        end();
}
</code>

此基于字符串的解决方案以及哈希片段的修剪即使在带有查询字符串的URL上也能正常工作,因为哈希部分在URL的一般结构中位于它们之后。

正如我之前所说,这只是实现此测试的一种可能方法。例如,本教程开头引用的文章使用了不同的解决方案,对链接href与location对象进行了组件级比较。

应该注意的是,我们在两种事件订阅方法中都使用了此函数,但在第二种方法中,我们将其用作我们已经知道是标签的元素的过滤器,因此对tagName属性的第一次检查是多余的。这留给读者作为练习。

可访问性注意事项

就目前而言,我们的代码容易受到已知错误(实际上是一对无关的错误,影响Blink/WebKit/KHTML和一个影响IE的错误)的影响,这些错误会影响键盘用户。当通过制表键浏览TOC链接时,激活一个链接将平滑地向下滚动到选定的部分,但焦点将保留在链接上。这意味着在下一个制表键按下时,用户将被送回TOC,而不是送往他们选择的节中的第一个链接。

为了解决这个问题,我们将向主脚本添加另一个函数:

<code>>
    <h1 id="gt">></h1>Title>
    <nav> id="toc"></nav>
        <ul>></ul>
            <li>></li>
<a> href="https://www.php.cn/link/db8229562f80fbcc7d780f571e5974ec"></a>Section 1>>
            <li>></li>
<a> href="https://www.php.cn/link/ba2cf4148007ed8a8b041f8abd9bbf96"></a>Section 2>>
            ...
        >
    >
     id="sect-1">
        <h2 id="gt">></h2>Section 1>
        <p>></p>Pellentesque habitant morbi tristique senectus et netus et <a> href="https://www.php.cn/link/e1b97c787a5677efa5eba575c41e8688"></a>a link to another page> ac turpis egestas. <a> href="https://www.php.cn/link/e1b97c787a5677efa5eba575c41e8688index.html#foo"></a>A link to another page, with an anchor> quam, feugiat vitae, ...>
        <a> href="https://www.php.cn/link/7421d74f57142680e679057ddc98edf5"></a>Back to TOC>
    >
     id="sect-2">
        <h2 id="gt">></h2>Section 2>
        ...
    >
    ...
     src="jump.js">>
     src="script.js">>
>
</code>

它将在我们将传递给jump函数的回调中运行,并将我们要滚动到的元素的哈希值传递过去:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

此函数的作用是获取哈希值对应的DOM元素,并测试它是否已经是可以接收焦点的元素(例如锚点或按钮元素)。如果元素不能默认接收焦点(例如我们的容器),则它会将其tabIndex属性设置为-1(允许通过编程方式接收焦点,但不能通过键盘接收)。然后焦点将设置为该元素,这意味着用户的下一个tab按键将焦点移动到下一个可用链接。

您可以在此处查看主脚本的完整源代码,其中包含所有先前讨论的更改。

使用CSS支持原生平滑滚动

CSS对象模型视图模块规范引入了一个新的属性来原生实现平滑滚动:scroll-behavior

它可以取两个值,auto表示默认的瞬时滚动,smooth表示动画滚动。该规范没有提供任何配置滚动动画的方法,例如其持续时间和时间函数(缓动)。

我可以使用css-scroll-behavior吗?来自caniuse.com的数据显示主要浏览器对css-scroll-behavior功能的支持情况。

不幸的是,在撰写本文时,支持非常有限。在Chrome中,此功能正在开发中,可以通过在chrome://flags屏幕中启用它来使用部分实现。CSS属性尚未实现,因此链接点击上的平滑滚动不起作用。

无论如何,通过对主脚本进行微小的更改,我们可以检测用户代理中是否可用此功能并避免运行我们的其余代码。为了在视口中使用平滑滚动,我们将CSS属性应用于根元素HTML(但在我们的测试页面中,我们甚至可以将其应用于body元素):

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

然后,我们在脚本开头添加一个简单的功能检测测试:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

因此,如果浏览器支持原生滚动,则脚本将不执行任何操作并退出,否则它将像以前一样继续执行,并且浏览器将忽略不受支持的CSS属性。

结论

除了实现简单和性能之外,刚才讨论的CSS解决方案的另一个优势是浏览器历史行为与使用浏览器默认滚动时所体验的行为一致。每个页面内跳转都推送到浏览器历史堆栈上,我们可以使用相应的按钮来回浏览这些跳转(但至少在Firefox中没有平滑滚动)。

在我们编写的代码中(我们现在可以将其视为CSS支持不可用时的后备方案),我们没有考虑脚本相对于浏览器历史记录的行为。根据上下文和用例,这可能是或可能不是感兴趣的事情,但如果我们认为脚本应该增强默认滚动体验,那么我们应该期望一致的行为,就像CSS一样。

关于使用原生JavaScript进行平滑滚动的常见问题解答 (FAQs)

如何在不使用任何库的情况下使用原生JavaScript实现平滑滚动?

在不使用任何库的情况下使用原生JavaScript实现平滑滚动非常简单。您可以使用window.scrollTo方法,并将behavior选项设置为smooth。此方法通过给定数量滚动窗口中的文档。这是一个简单的示例:

<code>import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed       ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}
</code>

在此示例中,当您点击具有类your-element的元素时,页面将平滑地滚动到顶部。

为什么我的平滑滚动在Safari中不起作用?

使用scrollTo方法并将behavior选项设置为smooth的平滑滚动功能在Safari中不受支持。要使其正常工作,您可以使用polyfill,例如smoothscroll-polyfill。这将在原生不支持它的浏览器中启用平滑滚动功能。

如何平滑地滚动到特定元素?

要平滑地滚动到特定元素,您可以使用Element.scrollIntoView方法,并将behavior选项设置为smooth。这是一个示例:

<code>var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed               ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t         t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();
</code>

在此示例中,当您点击具有类your-element的元素时,页面将平滑地滚动到具有类target-element的元素。

我可以控制平滑滚动的速度吗?

平滑滚动的速度不能直接控制,因为它由浏览器处理。但是,您可以使用window.requestAnimationFrame创建一个自定义平滑滚动函数,以便更好地控制滚动动画,包括其速度。

如何实现水平平滑滚动?

您可以通过与垂直平滑滚动类似的方式实现水平平滑滚动。window.scrollToElement.scrollIntoView方法也接受left选项以指定要滚动到的水平位置。这是一个示例:

<code>function jump(target, options) {
    var start = window.pageYOffset;

    var opt = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    };

    var distance = typeof target === 'string' ? 
        opt.offset + document.querySelector(target).getBoundingClientRect().top : 
        target
    ;

    var duration = typeof opt.duration === 'function'
          ? opt.duration(distance)
          : opt.duration
    ;

    var 
        timeStart = null,
        timeElapsed
    ;

    requestAnimationFrame(loop);

    function loop(time) {
        if (timeStart === null)
            timeStart = time;

        timeElapsed = time - timeStart;

        window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

        if (timeElapsed         requestAnimationFrame(loop)
        else
            end();
    }

    function end() {
        window.scrollTo(0, start + distance);

        typeof opt.callback === 'function' && opt.callback();
        timeStart = null;
    }

    // ...

}
</code>

这将使文档向右平滑滚动100像素。

如何停止平滑滚动动画?

不能直接停止平滑滚动动画,因为它由浏览器处理。但是,如果您使用的是自定义平滑滚动函数,则可以使用window.cancelAnimationFrame取消动画帧来停止动画。

如何实现具有固定页眉的平滑滚动?

要实现具有固定页眉的平滑滚动,您需要调整滚动位置以考虑页眉的高度。您可以通过从目标滚动位置减去页眉的高度来实现此目的。

如何为锚链接实现平滑滚动?

要为锚链接实现平滑滚动,您可以向链接的点击事件添加事件侦听器,并使用Element.scrollIntoView方法平滑地滚动到目标元素。这是一个示例:

<code>requestAnimationFrame(function(time) { timeStart = time; loop(time); });

function loop(time) {
    timeElapsed = time - timeStart;

    window.scrollTo(0, opt.easing(timeElapsed, start, distance, duration));

    if (timeElapsed         requestAnimationFrame(loop)
    else
        end();
}
</code>

这将使页面上的所有锚链接平滑地滚动到其目标元素。

如何使用键盘导航实现平滑滚动?

使用键盘导航实现平滑滚动比较复杂,因为它需要拦截键盘事件并手动滚动文档。您可以通过向keydown事件添加事件侦听器并使用window.scrollTo方法平滑地滚动文档来实现此目的。

如何测试我的平滑滚动实现的兼容性?

您可以使用BrowserStack等在线工具测试平滑滚动实现的兼容性。这些工具允许您在不同的浏览器和不同的设备上测试您的网站,以确保您的实现可以在所有环境中正常工作。

以上是如何在Vanilla JavaScript中实现光滑的滚动的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
Java vs JavaScript:开发人员的详细比较Java vs JavaScript:开发人员的详细比较May 16, 2025 am 12:01 AM

javaandjavascriptaredistinctlanguages:javaisusedforenterpriseandmobileapps,while javascriptifforInteractiveWebpages.1)JavaisComcompoppored,statieldinglationallyTypted,statilly tater astrunsonjvm.2)

JavaScript数据类型:浏览器和nodejs之间是否有区别?JavaScript数据类型:浏览器和nodejs之间是否有区别?May 14, 2025 am 12:15 AM

JavaScript核心数据类型在浏览器和Node.js中一致,但处理方式和额外类型有所不同。1)全局对象在浏览器中为window,在Node.js中为global。2)Node.js独有Buffer对象,用于处理二进制数据。3)性能和时间处理在两者间也有差异,需根据环境调整代码。

JavaScript评论:使用//和 / * * / * / * /JavaScript评论:使用//和 / * * / * / * /May 13, 2025 pm 03:49 PM

JavaScriptusestwotypesofcomments:single-line(//)andmulti-line(//).1)Use//forquicknotesorsingle-lineexplanations.2)Use//forlongerexplanationsorcommentingoutblocksofcode.Commentsshouldexplainthe'why',notthe'what',andbeplacedabovetherelevantcodeforclari

Python vs. JavaScript:开发人员的比较分析Python vs. JavaScript:开发人员的比较分析May 09, 2025 am 12:22 AM

Python和JavaScript的主要区别在于类型系统和应用场景。1.Python使用动态类型,适合科学计算和数据分析。2.JavaScript采用弱类型,广泛用于前端和全栈开发。两者在异步编程和性能优化上各有优势,选择时应根据项目需求决定。

Python vs. JavaScript:选择合适的工具Python vs. JavaScript:选择合适的工具May 08, 2025 am 12:10 AM

选择Python还是JavaScript取决于项目类型:1)数据科学和自动化任务选择Python;2)前端和全栈开发选择JavaScript。Python因其在数据处理和自动化方面的强大库而备受青睐,而JavaScript则因其在网页交互和全栈开发中的优势而不可或缺。

Python和JavaScript:了解每个的优势Python和JavaScript:了解每个的优势May 06, 2025 am 12:15 AM

Python和JavaScript各有优势,选择取决于项目需求和个人偏好。1.Python易学,语法简洁,适用于数据科学和后端开发,但执行速度较慢。2.JavaScript在前端开发中无处不在,异步编程能力强,Node.js使其适用于全栈开发,但语法可能复杂且易出错。

JavaScript的核心:它是在C还是C上构建的?JavaScript的核心:它是在C还是C上构建的?May 05, 2025 am 12:07 AM

javascriptisnotbuiltoncorc; saninterpretedlanguagethatrunsonenginesoftenwritteninc.1)javascriptwasdesignedAsalightweight,解释edganguageforwebbrowsers.2)Enginesevolvedfromsimpleterterterpretpreterterterpretertestojitcompilerers,典型地提示。

JavaScript应用程序:从前端到后端JavaScript应用程序:从前端到后端May 04, 2025 am 12:12 AM

JavaScript可用于前端和后端开发。前端通过DOM操作增强用户体验,后端通过Node.js处理服务器任务。1.前端示例:改变网页文本内容。2.后端示例:创建Node.js服务器。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )专业的PHP集成开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。