关于滚动条你知道多少

170 阅读4分钟

滚动条可以是window(浏览器)的,也可以是单个div的。基本上二者性质一致。来自AI的回答:

浏览器的滚动条和div的滚动条在功能上是相同的,都是用来在有限的视图区域内浏览更多的内容。但在使用和样式上,它们有一些区别:

  1. 使用上的区别:浏览器的滚动条是用来控制整个网页视图的滚动,而div的滚动条是用来控制div元素内部的滚动。当网页或div的内容超出其视图区域时,浏览器会自动显示滚动条。
  2. 样式上的区别:浏览器的滚动条样式通常是由浏览器或操作系统决定的,不同的浏览器和操作系统的滚动条样式可能会有所不同。而div的滚动条样式可以通过css来自定义,例如可以使用::-webkit-scrollbar伪元素来自定义滚动条的样式(注意这是Webkit内核浏览器的特性,不是所有浏览器都支持)。
  3. 交互上的区别:浏览器的滚动条通常会响应鼠标滚轮和键盘上下键的操作,而div的滚动条只有在div获得焦点时才会响应这些操作。
  4. 性能上的区别:频繁的div滚动可能会导致页面重排和重绘,影响页面性能,而浏览器的滚动通常不会有这个问题。

针对滚动条有一些典型的开发需求场景。

判断滚动条是否到底

这里判断的是浏览器滚动条而不是单个div的。下同。

window.onscroll = function() {
    // 滚动的高度
    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    // dom的高度
    const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
    // 可视区高度
    const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
    if (scrollTop + clientHeight >= scrollHeight) {
      console.log('Scrolled to bottom');
    }
  };

首先获取滚动元素的scrollTopclientHeightscrollHeight属性。scrollTop属性表示元素已经滚动过的距离,clientHeight属性表示元素的可视区域的高度,scrollHeight属性表示元素的总高度。

然后比较scrollTop + clientHeightscrollHeight,如果它们相等或者前者大于后者,那么就表示滚动条已经滚动到底部了。

判断滚动条滚动的方向

let lastScrollTop = 0;
window.addEventListener('scroll', function() {
  let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  if (scrollTop > lastScrollTop){
    console.log('scrolling down');
  } else {
    console.log('scrolling up');
  }
  lastScrollTop = scrollTop;
});

首先定义一个变量lastScrollTop来保存上一次的滚动位置。然后监听windowscroll事件。在每次滚动时,我们获取当前的滚动位置,然后与上一次的滚动位置进行比较,以判断滚动的方向。最后,更新上一次的滚动位置。

这个方法只能判断垂直滚动的方向。如果想要判断水平滚动的方向,可以使用scrollLeft属性代替scrollTop属性。

滚动条滚动到锚点

只要在url后面加上hash就可以让滚动条滚动到对应的锚点,再代码上需要hrefid关联起来。

<a href="#section1">Go to Section 1</a>
<a href="#section2">Go to Section 2</a>

<div id="section1" style="height: 2000px; background: lightblue;">
  <h1>Section 1</h1>
</div>

<div id="section2" style="height: 2000px; background: lightgreen;">
<h1>Section 2</h1>

但上面的方式并非滚动的而是瞬间到达锚点位置。想要平滑滚动可以添加代码

document.querySelectorAll('a').forEach(function(link) {
  link.addEventListener('click', function(event) {
    event.preventDefault();
    var target = document.querySelector(this.getAttribute('href'));
    window.scrollTo({
      top: target.offsetTop,
      behavior: 'smooth' // 平滑实现
    });
  });
})

获取到所有的链接元素,然后为每个链接添加一个click事件监听器。当点击链接时,阻止链接的默认行为(即跳转到锚点),然后获取到锚点元素,最后使用window.scrollTo方法将页面滚动到锚点的位置。

滚动条各种运动方式回到顶部

这是之前总结实现的,实际是补间动画,当触发回到顶部时,将运动变化规则化,从而实现各种形式的运动回到顶部。

需要借助tween.js提供的一系列缓动函数。详情可参考:easings.net/zh-cn

/*
 * Tween.js
 * t: current time(当前时间);
 * b: beginning value(初始值);
 * c: change in value(变化量);
 * d: duration(持续时间)。
 * you can visit 'http://easings.net/zh-cn' to get effect
*/
var Tween = {
  Linear: function (t, b, c, d) {
    return c * t / d + b
  },
  Quad: {
    easeIn: function (t, b, c, d) {
      return c * (t /= d) * t + b
    },
    easeOut: function (t, b, c, d) {
      return -c * (t /= d) * (t - 2) + b
    },
    easeInOut: function (t, b, c, d) {
      if ((t /= d / 2) < 1) return c / 2 * t * t + b
      return -c / 2 * ((--t) * (t - 2) - 1) + b
    }
  },
  Cubic: {
    easeIn: function (t, b, c, d) {
      return c * (t /= d) * t * t + b
    },
    easeOut: function (t, b, c, d) {
      return c * ((t = t / d - 1) * t * t + 1) + b
    },
    easeInOut: function (t, b, c, d) {
      if ((t /= d / 2) < 1) return c / 2 * t * t * t + b
      return c / 2 * ((t -= 2) * t * t + 2) + b
    }
  },
  Quart: {
    easeIn: function (t, b, c, d) {
      return c * (t /= d) * t * t * t + b
    },
    easeOut: function (t, b, c, d) {
      return -c * ((t = t / d - 1) * t * t * t - 1) + b
    },
    easeInOut: function (t, b, c, d) {
      if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b
      return -c / 2 * ((t -= 2) * t * t * t - 2) + b
    }
  },
  Quint: {
    easeIn: function (t, b, c, d) {
      return c * (t /= d) * t * t * t * t + b
    },
    easeOut: function (t, b, c, d) {
      return c * ((t = t / d - 1) * t * t * t * t + 1) + b
    },
    easeInOut: function (t, b, c, d) {
      if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b
      return c / 2 * ((t -= 2) * t * t * t * t + 2) + b
    }
  },
  Sine: {
    easeIn: function (t, b, c, d) {
      return -c * Math.cos(t / d * (Math.PI / 2)) + c + b
    },
    easeOut: function (t, b, c, d) {
      return c * Math.sin(t / d * (Math.PI / 2)) + b
    },
    easeInOut: function (t, b, c, d) {
      return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b
    }
  },
  Expo: {
    easeIn: function (t, b, c, d) {
      return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b
    },
    easeOut: function (t, b, c, d) {
      return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b
    },
    easeInOut: function (t, b, c, d) {
      if (t == 0) return b
      if (t == d) return b + c
      if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b
      return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b
    }
  },
  Circ: {
    easeIn: function (t, b, c, d) {
      return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b
    },
    easeOut: function (t, b, c, d) {
      return c * Math.sqrt(1 - (t = t / d - 1) * t) + b
    },
    easeInOut: function (t, b, c, d) {
      if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b
      return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b
    }
  },
  Elastic: {
    easeIn: function (t, b, c, d, a, p) {
      var s
      if (t == 0) return b
      if ((t /= d) == 1) return b + c
      if (typeof p === 'undefined') p = d * 0.3
      if (!a || a < Math.abs(c)) {
        s = p / 4
        a = c
      } else {
        s = p / (2 * Math.PI) * Math.asin(c / a)
      }
      return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b
    },
    easeOut: function (t, b, c, d, a, p) {
      var s
      if (t == 0) return b
      if ((t /= d) == 1) return b + c
      if (typeof p === 'undefined') p = d * 0.3
      if (!a || a < Math.abs(c)) {
        a = c
        s = p / 4
      } else {
        s = p / (2 * Math.PI) * Math.asin(c / a)
      }
      return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b)
    },
    easeInOut: function (t, b, c, d, a, p) {
      var s
      if (t == 0) return b
      if ((t /= d / 2) == 2) return b + c
      if (typeof p === 'undefined') p = d * (0.3 * 1.5)
      if (!a || a < Math.abs(c)) {
        a = c
        s = p / 4
      } else {
        s = p / (2 * Math.PI) * Math.asin(c / a)
      }
      if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b
      return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b
    }
  },
  Back: {
    easeIn: function (t, b, c, d, s) {
      if (typeof s === 'undefined') s = 1.70158
      return c * (t /= d) * t * ((s + 1) * t - s) + b
    },
    easeOut: function (t, b, c, d, s) {
      if (typeof s === 'undefined') s = 1.70158
      return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b
    },
    easeInOut: function (t, b, c, d, s) {
      if (typeof s === 'undefined') s = 1.70158
      if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b
      return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b
    }
  },
  Bounce: {
    easeIn: function (t, b, c, d) {
      return c - Tween.Bounce.easeOut(d - t, 0, c, d) + b
    },
    easeOut: function (t, b, c, d) {
      if ((t /= d) < (1 / 2.75)) {
        return c * (7.5625 * t * t) + b
      } else if (t < (2 / 2.75)) {
        return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b
      } else if (t < (2.5 / 2.75)) {
        return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b
      } else {
        return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b
      }
    },
    easeInOut: function (t, b, c, d) {
      if (t < d / 2) {
        return Tween.Bounce.easeIn(t * 2, 0, c, d) * 0.5 + b
      } else {
        return Tween.Bounce.easeOut(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b
      }
    }
  }
}

当点击回到顶部,触发方法。通过切换缓动函数实现各种运动形式的回到顶部。


    gotoPageTop () {
      const scrollTop =  document.documentElement.scrollTop ||
        document.body.scrollTop
      let t = 0 // 开始时间
      const b = scrollTop // 开始位置 
      const c = -scrollTop // 变化值
      const d = 60
      const doc = document
      const win = window
      let reAnition = null
      const step = () => {
        const value = Tween.Cubic.easeOut(t, b, c, d)
        if (doc.documentElement.scrollTop) {
          doc.documentElement.scrollTop = value
        }
        if (doc.body.scrollTop) {
          doc.body.scrollTop = value
        }
        t++
        if (t <= d) {
          // 继续运动 使用requestAnimationFrame
          reAnition = win.requestAnimationFrame(step)
        } else {
          win.cancelAnimationFrame(reAnition)
          // 动画结束
        }
      }
      step()
    }

自制滑块-水平滚动条

<div id="container">
  <div id="content">Your content here...</div>
</div>
<div id="slider">
  <div id="handle"></div>
</div>
<style>
#container {
  width: 100%;
  overflow: auto;
}
#content { 
  height: 50px;
  width: 200%; /* make it wider than container to enable horizontal scroll */
}
#slider {
  width: 100%;
  height: 20px;
  background: #ccc;
  position: relative;
}
#handle {
  width: 50px;
  height: 20px;
  background: #333;
  cursor: pointer;
  position: absolute;
}
</style>

首先获取到容器元素(container)、滑块元素(slider)和手柄元素(handle)。然后设置手柄的宽度为滑块宽度乘以容器宽度除以容器滚动宽度,这样手柄的位置就可以对应到容器的滚动位置。

let container = document.getElementById('container');
let slider = document.getElementById('slider');
let handle = document.getElementById('handle');

// update handle width when content width changes
handle.style.width = slider.offsetWidth * container.clientWidth / container.scrollWidth + 'px';

添加一个mousedown事件监听器到手柄上,当鼠标按下时,记录下当前的鼠标位置和滚动位置,然后在windowmousemove事件中,根据鼠标移动的距离来更新滚动位置。

最后添加一个scroll事件监听器到容器上,当容器的滚动位置改变时,将手柄的位置设置为滑块宽度乘以滚动位置除以滚动宽度。

这样无论是拖动手柄还是滚动容器,都会同步更新另一个元素的位置,实现了手柄和真实滚动条的同步


// drag handle to scroll
handle.onmousedown = function(e) {
  let startX = e.clientX;
  let startLeft = container.scrollLeft;

  window.onmousemove = function(e) {
    let deltaX = e.clientX - startX;
    container.scrollLeft = startLeft + deltaX * container.scrollWidth / slider.offsetWidth;
  };

  window.onmouseup = function() {
    window.onmousemove = null;
    window.onmouseup = null;
  };
};

// scroll to move handle
container.onscroll = function() {
  handle.style.left = slider.offsetWidth * this.scrollLeft / this.scrollWidth + 'px';
};

本文完。