原文链接:http://javascript.info/onscroll,translate with ❤️ by zhangbao.

滚动事件允许我们在滚动页面或元素时做出响应,我们可以以此为契机,做很多事情。

例如:

  • 根据文档现在的滚动位置,展示/隐藏相关控件和信息。

  • 当滚动到页面底部时,加载更多数据。

下面是显示当前滚动偏移量的小函数:

  1. window.addEventListener('scroll', function () {
  2. document.getElementById('showScroll').innerHTML = pageYOffset + 'px';
  3. });

scroll 事件可以绑定在 window 和其他所有的可滚动元素上。

阻止滚动

我们怎样阻止滚动呢?如果在 onscroll 事件处理器中使用 event.preventDefault() 阻止滚动是行不通的,因为这个事件是在滚动之后触发的。

但我们可以在引起滚动的事件的事件处理器中使用 event.preventDefault() 阻止滚动发生。

例如:

  • wheel 事件——滚动鼠标滚轮时(一个“滚动”触摸板动作也会生成它)。

  • keydown 事件—— 按下 pageUppageDown 按键时。

有些时候这有些帮助。但滚动方式有多种,很难处理所有情况。最可靠的方式是使用 CSS 阻止滚动,比如使用 overflow 属性。

下面布置了一些任务,供你解决并体会 onscroll 在应用程序中的使用。

练习题

问题

一、无限滚动页面

创建一个可以无限滚动的页面。当用户滚动到页面底部的时候,自动在尾部添加时间的文本表示,这样用户就可以进一步滚动页面了。

滚动时,有两个重要的特性点:

  1. 滚动是“灵活的”。在某些浏览器/设备中,我们可以稍微超出文档开始或结束的范围滚动(显示下面的空白区域,然后文档将自动“弹回”到正常状态)。

  2. 滚动是不精确的。当我们滚动到页面末尾时,我们可能实际上距离真正的文档底部有 0~50px 的距离。

因此,“滚动到底部”表示用户滚动到距离文档底部不超过 100px 的距离。

P.S. 在实际场景下,通过显示的是“更多消息”或“更多商品”之类的。

二、“回到顶部”按钮

创建一个“回到顶部”按钮帮助页面滚动。

它的行为如下:

  1. 当页面滚动没有超过一个窗口高度的时候,这个按钮是隐藏的。

  2. 当页面滚动超过一个窗口高度了,在左上角显示“回到顶部”按钮。当页面滚动回去了,按钮消失。

  3. 点击按钮的时候,页面滚动到顶部。

三、图片懒加载

我假设我们有一个速度较慢的客户端,希望节省他们的移动通信流量。

为了这目的,我们不准备立即显示图片,而是先代之以一个占位图片,想这样:

  1. <img src="placeholder.svg" width="128" height="128" data-src="real.jpg">

因此一开始,所有图片都引用 palceholder.svg。当页面滚动到可以看见图片地方的时候——我们将 data-src 属性值替换 src,这时才加载图片。

滚动 - 图1

点击滚动查看图片懒加载效果。

要求:

  • 当页面加载的时候,当前视口内的看见图片应立即显示,而不是在滚动之后。

  • 一些图片可能无需懒加载效果,它们没有 data-src 属性,我们无需处理它们。

  • 图片一旦加载,当再次滚动进入/离开的时候,都不要再重新加载了。

P.S. 如果可以的话,可以提供一个更加高级的解决方案——“预加载”当前位置的下方/之后一页的图片。

P.P.S 只需处理垂直滚动就可以了,不用管水平滚动的情况。

答案

一、无限滚动页面

解决方案的核心就是当我们滚动到页面底部的时候,有一个函数用来向页面中添加更多日期(实际情景下可能是其他内容)。

我们理解调用这个函数,并且添加一个 window.onscroll 处理程序。

最重要的问题是:“我们怎么探测页面已经滚动到底部了”。

这里就要用到窗口坐标了。

文档是表现(包含)在 <html> 标签中的,也就是 document.documentElement

我们可以使用 document.documentElement.getBoundingClientRect() 获取文档在窗口坐标中的相对位置。其中的 .bottom 属性表示文档底部距离窗口顶部的距离。

例如,如果整个文档的高度是 2000px,那么:

  1. // 当文档处于页面顶部的时候
  2. // 相对窗口的 top 坐标值是 0
  3. document.documentElement.getBoundingClientRect().top = 0
  4. // 而相对窗口的 bottom 坐标值则是 2000
  5. // 文档很长,因此在窗口范围之外
  6. document.documentElement.getBoundingClientRect().bottom = 2000

如果我们向下滚动 500px,那么:

  1. // 现在文档顶部距离窗口顶部 500px
  2. document.documentElement.getBoundingClientRect().top = -500
  3. // 现在文档底部距离窗口顶部近了 500px
  4. document.documentElement.getBoundingClientRect().bottom = 1500

当我们将文档滚动到底部的时候,假设此时窗口高度是 600px

  1. // 现在文档顶部距离窗口底部的距离是 1400px
  2. document.documentElement.getBoundingClientRect().top = -1400
  3. // 现在文档底部距离窗口顶部的距离是 600px
  4. document.documentElement.getBoundingClientRect().bottom = 600

需要注意的是,bottom 的值不能是 0,因此不会到达窗口顶部。bottom 坐标值限制在最小一个窗口的高度,因为我们无法再向下滚动了。

窗口高度使用 document.documentElement.clientHeight 获得。

我们想要保证窗口底部和文档底部之间至少 100px 远的距离,也就是窗口顶部距离文档底部的距离至少是
(document.documentElement.clientHeight + 100) px 这么远。

下面是函数实现:

  1. function populate() {
  2. while(true) {
  3. // 获取文档底部到窗口顶部的距离
  4. let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom;
  5. // 如果这个距离大于 一个窗口高度 + 100px,说明我们还没有到达页面底部
  6. // 无需加载更多数据
  7. if (windowRelativeBottom > document.documentElement.clientHeight + 100) break;
  8. // 否则加载更多数据(加载数后,while 循环继续下次迭代,因为添加了一个内容让距离变大了,下次迭代时就 break 了)
  9. document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
  10. }
  11. }

这里是完整代码:

  1. function populate() {
  2. while(true) {
  3. let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom;
  4. if (windowRelativeBottom > document.documentElement.clientHeight + 100) break;
  5. document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
  6. }
  7. }
  8. window.addEventListener('scroll', populate);
  9. populate(); // 初始化

在线查看

二、“回到顶部”按钮

  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>
  4. <style>
  5. body,
  6. html {
  7. height: 100%;
  8. width: 100%;
  9. padding: 0;
  10. margin: 0;
  11. }
  12. #matrix {
  13. width: 400px;
  14. margin: auto;
  15. overflow: auto;
  16. text-align: justify;
  17. }
  18. #arrowTop {
  19. height: 9px;
  20. width: 14px;
  21. color: green;
  22. position: fixed;
  23. top: 10px;
  24. left: 10px;
  25. cursor: pointer;
  26. }
  27. #arrowTop::before {
  28. content: '▲';
  29. }
  30. </style>
  31. <meta charset="utf-8">
  32. </head>
  33. <body>
  34. <div id="matrix">
  35. <script>
  36. for (let i = 0; i < 2000; i++) document.writeln(i)
  37. </script>
  38. </div>
  39. <div id="arrowTop" hidden></div>
  40. <script>
  41. arrowTop.onclick = function() {
  42. window.scrollTo(window.pageXOffset, 0);
  43. // 使用 scrollTo 会触发 scroll 事件,因此箭头会自动隐藏
  44. };
  45. window.addEventListener('scroll', function() {
  46. arrowTop.hidden = (window.pageYOffset < document.documentElement.clientHeight);
  47. });
  48. </script>
  49. </body>
  50. </html>

在线查看

三、图片懒加载

onscroll 处理器用来检查哪些图片处于可视区范围、并显示。

当页面加载后也需要执行它,立即检查当前可视区区域有无图片而不是等到滚动了再加载。

我们将它放在 <body> 底部,它会在页面内容加载完毕后执行。

  1. // ...页面内容在这上面...
  2. function isVisible(elem) {
  3. let coords = elem.getBoundingClientRect();
  4. let windowHeight = document.documentElement.clientHeight;
  5. // 元素的顶部边缘是可见的 或 元素的底部边缘是可见的
  6. let topVisible = coords.top > 0 && coords.top < windowHeight;
  7. let bottomVisible = coords.bottom > 0 && coords.bottom < windowHeight;
  8. return topVisible || bottomVisible;
  9. }
  10. showVisible();
  11. window.onscroll = showVisible;

对在可视区区域的图片可以这样处理:将 img.dataset.src 的属性值赋给 img.src(如果没做的话)。

P.S. 解决方案有一个变体 isVisible——“预加载”下面/上面一页里的图片(窗口高度使用 document.documentElement.clientHeight 获取)。

  1. /**
  2. * 检查元素是否可见(在页面的可视区范围之内)
  3. * 元素的顶部或者底部在可视区范围都认为是可见的
  4. */
  5. function isVisible(elem) {
  6. let coords = elem.getBoundingClientRect();
  7. let windowHeight = document.documentElement.clientHeight;
  8. // 元素的顶部可见 或 元素底部可见
  9. let topVisible = coords.top > 0 && coords.top < windowHeight;
  10. let bottomVisible = coords.bottom < windowHeight && coords.bottom > 0;
  11. return topVisible || bottomVisible;
  12. }
  13. /**
  14. 一个变体:检查当前可视区上&下一页的图片,并提前加载起来!
  15. function isVisible(elem) {
  16. let coords = elem.getBoundingClientRect();
  17. let windowHeight = document.documentElement.clientHeight;
  18. let extendedTop = -windowHeight;
  19. let extendedBottom = 2 * windowHeight;
  20. // 顶部可见 || 底部可见
  21. let topVisible = coords.top > extendedTop && coords.top < extendedBottom;
  22. let bottomVisible = coords.bottom > extendedTop && coords.bottom < extendedBottom;
  23. return topVisible || bottomVisible;
  24. }
  25. */
  26. function showVisible() {
  27. for (let img of document.querySelectorAll('img')) {
  28. let realSrc = img.dataset.src;
  29. // 加载后的图片不再处理
  30. if (!realSrc) continue;
  31. if (isVisible(img)) {
  32. // 禁用缓存
  33. // 这行代码在生产环境下请删除
  34. realSrc += '?nocache=' + Math.random();
  35. img.src = realSrc;
  36. img.dataset.src = '';
  37. }
  38. }
  39. }
  40. window.addEventListener('scroll', showVisible);
  41. showVisible();

(完)