首頁 > web前端 > js教程 > 如何在Vanilla JavaScript中實現光滑的滾動

如何在Vanilla JavaScript中實現光滑的滾動

William Shakespeare
發布: 2025-02-18 10:49:09
原創
507 人瀏覽過

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>></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>></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>></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
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板