JS: Scroll events

筆記學到的三種可以處理scroll事件的技巧,分別是:

  1. window.scrollTo() + window.pageYOffset
  2. Element.scrollIntoView()
  3. Intersection observer API

前兩個技巧用來實作smooth scrolling;第3個則是要實作捲動到某個位置出現固定導覽列(sticky bar)的效果


Smooth Scrolling

1. window.scrollTo()

第一種是過去常實作的smooth scrolling方法,適合用在舊版瀏覽器:

const divElem = document.querySelector('#divElem');
const divCoords = divElem.getBoundingClientRect();
window.scrollTo(
    divCoords.left + window.pageXOffset,
    divCoords.top + window.pageYOffset
);

// Smooth scrolling
// window.scrollTo({
//     left: divCoords.left + window.pageXOffset,
//     top: divCoords.top + window.pageYOffset,
//     behavior: 'smooth'
// });

如果瀏覽器有支援方法2或3,儘量不要使用這種方法,因為 window.pageYOffset 這行會在捲動頁面時一直觸發捲動事件,造成頁面效能變差。

2. Element.scrollIntoView()

第二個實作smooth scrolling方法是現在比較常用的方法,不會一直觸發捲動事件又容易實作,只是要注意瀏覽器是否支援,尤其Safari並不支援方法的optional參數:

const divElem = document.querySelector('#divElem');

divElem.scrollIntoView();

// Smooth scrolling
// divElem.scrollIntoView({behavior: 'smooth'});



Showing Sticky Navigation Bar

3. Intersection API

第三個scroll事件要用Web API裡的Intersection API來實作「當使用者捲動頁面,使得頁面viewport一離開header元素頂部時,就顯示固定導覽列」的效果:

const header = document.querySelector('.header');
const navBar = document.querySelector('#navBar');   // target element
const navBarHeight = Number.parseInt(getComputedStyle(navBar).height)   // 取得導覽列高度

// Intersection API setting
const callback = function(entries){
    const [entry] = entries;
    if(!entry.isIntersecting) {
        navbar.styles.position = "fixed"
    } else {
        navbar.styles.position = "static"
    }
}

const options = {
    root: null   // Intersected element (null represents monitoring the changes that target element intersecting to device's viewport)
    , threshold: 0.9   // Intersection ratio, the degree of intersection between the target element and its root (between 0.0 and 1.0).
    , rootMargin: '0px'   // Margin around the root (0 is default value)
}

const headerElemObserver = new IntersectionObserver(callback, options);
headerElemObserver.observe(header);

thread: 0.9 是指 viewport 和 header元素(高度)的交集比例少於90%就會顯示固定導覽列;這裡我沒有設定成 thread: 1,因為 1 代表只要 viewport 和 header 一沒有交集 就會出現固定導覽列,但一和整個header有交集就會讓導覽列恢復原狀,某些情況下容易導致衝突發生,因此設定成 thread: 0.9。 (註:若要驗證設定成1會出現什麼狀況,可以慢慢改變視窗大小,會發現在某個視窗大小會反覆出現intersection衝突)

這裡要注意的是,headerElemObserver.observe(header) 裡不能給nav,因為導覽列轉換成固定導覽列之後其實還是會和header有交集,如果這樣設定會導致衝突,因此這裡是給 header

個人覺得這是蠻酷的方法😂,根據MDN的說明,Intersection API是用asynchronous方式去追蹤變化:

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

如此一來,比起第1個這種過去在main thread實作的常用方法,Intersection API顯得有效率多了~

另外,Intersection API不一定只能用在範例的捲動事件,有些常見的網頁效果也可以這個API去實作,像是:

  • Lazy-loading of images or other content
  • Infinite scrolling
  • Reporting of visibility of advertisements
  • Decisions for performing tasks or animation processes

References The Complete JavaScript Course 2022: From Zero to Expert! Window.scrollTo() Element.scrollIntoView() Intersection Observer API Building A Dynamic Header With Intersection Observer Sticky Header using Intersection Observer API