2020年11月13日

滚动

scroll 事件允许对页面或元素滚动作出反应。我们可以在这里做一些有用的事情。

例如:

  • 根据用户在文档中的位置显示/隐藏其他控件或信息。
  • 当用户向下滚动到页面末端时加载更多数据。

这是一个显示当前滚动的小函数:

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

在运行中:

Current scroll = scroll the window

scroll 事件在 window 和可滚动元素上都可以运行。

防止滚动

我们如何使某些东西变成不可滚动?

我们不能通过在 onscroll 监听器中使用 event.preventDefault() 来阻止滚动,因为它会在滚动发生 之后 才触发。

但是我们可以在导致滚动的事件上,例如在 pageUppageDownkeydown 事件上,使用 event.preventDefault() 来阻止滚动。

如果我们向这些事件中添加事件处理程序,并向其中添加 event.preventDefault(),那么滚动就不会开始。

启动滚动的方式有很多,使用 CSS 的 overflow 属性更加可靠。

有几个练习题,你可以解决或者浏览以下几个任务来看一下 onscroll 的应用。

任务

重要程度: 5

创建一个无限的页面。当访问者滚动到页面末端时,它会自动将当期日期时间附加到文本中(以便访问者可以滚动更多内容)。

像这样:

请注意滚动的两个重要特性:

  1. 滚动是“弹性的”。在某些浏览器/设备中,我们可以在文档的顶端或末端稍微多滚动出一点(超出部分显示的是空白区域,然后文档将自动“弹回”到正常状态)。
  2. 滚动并不精确。当我们滚动到页面末端时,实际上我们可能距真实的文档末端约 0-50px。

因此,“滚动到末端”应该意味着访问者离文档末端的距离不超过 100px。

P.S. 在现实生活中,我们可能希望显示“更多信息”或“更多商品”。

打开一个任务沙箱。

解决方案的核心是一个函数,当我们在页面末端时,该函数可以向页面添加更多日期(或者在实际开发中是加载更多内容)。

我们可以立即调用它,并将其添加为 window.onscroll 处理程序。

最重要的问题是:“如何检测页面滚动到了末端?”

让我们使用相对于窗口的坐标。

文档(document)在 <html> 标签中被表示(被包含)为 document.documentElement

我们可以通过 document.documentElement.getBoundingClientRect() 来获取整个文档相对于窗口的坐标。bottom 属性将是文档末端的相对于窗口的坐标。

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

// 当我们在页面顶端时
// 相对于窗口 top = 0
document.documentElement.getBoundingClientRect().top = 0

// 相对于窗口 bottom = 2000
// 如果文档太长,那么可能会远远超出窗口底部
document.documentElement.getBoundingClientRect().bottom = 2000

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

// 文档顶端在窗口之方 500px
document.documentElement.getBoundingClientRect().top = -500
// 文档末端相对于窗口近了 500px
document.documentElement.getBoundingClientRect().bottom = 1500

当我们滚动到文档末端时,假设窗口高度为 600px

// 文档顶端在窗口上方 -1400px
document.documentElement.getBoundingClientRect().top = -1400
// 文档末端相对于窗口坐标为 600px
document.documentElement.getBoundingClientRect().bottom = 600

请注意,bottom 不能为 0,因为它永远不会到达窗口顶部。bottom 坐标的最低限度是窗口高度(我们假设其为 600),我们无法再向上滚动了。

我们可以获得窗口的高度为 document.documentElement.clientHeight

对于本任务,我们需要知道何时文档末端距窗口底部不超过 100px(即,如果窗口高度为 600px,则为 600-700px)。

所以,函数如下:

function populate() {
  while(true) {
    // 文档末端
    let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom;

    // 如果用户将页面滚动的距离不够远(文档末端距窗口底部 >100px)
    if (windowRelativeBottom > document.documentElement.clientHeight + 100) break;

    // 让我们添加更多数据
    document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
  }
}

使用沙箱打开解决方案。

重要程度: 5

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

它应该像这样运行:

  • 页面向下滚动的距离没有超过窗口高度时 —— 按钮不可见。
  • 当页面向下滚动距离超过窗口高度时 —— 在左上角出现一个“向上”的箭头。如果页面回滚回去,箭头就会消失。
  • 单击箭头时,页面将滚动到顶部。

像这样(左上角,滚动查看):

打开一个任务沙箱。

重要程度: 4

假设我们有一个速度较慢的客户端,并且希望节省它们在移动端的流量。

为此,我们决定不立即显示图像,而是将其替换为占位符,如下所示:

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

因此,最初所有图像均为 placeholder.svg。当页面滚动到用户可以看到图像位置时 —— 我们就会将 src 更改为 data-srcsrc,从而加载图像。

这是在 iframe 中的一个示例:

滚动它可以看到图像是“按需”加载的。

要求:

  • 加载页面时,屏幕上的那些图像应该在滚动之前立即加载。
  • 有些图像可能是常规图像,没有 data-src。代码不应该改动它们。
  • 一旦图像被加载,它就不应该在滚动进/出时被重新加载。

P.S. 如果你有能力,可以创建一个更高级的解决方案,以“预加载”当前位置下方/之后一页的图像。

P.P.S. 仅处理垂直滚动,不处理水平滚动。

打开一个任务沙箱。

onscroll 处理程序应该检查哪些图像是可见的,并显示它们。

我们还希望在页面加载时运行它,以检测即将可见的图像并加载它们。

该代码应该在文档加载完成时执行,以便可以访问文档内容。

或者将该代码放在 <body> 底部:

// ...页面内容在上面...

function isVisible(elem) {

  let coords = elem.getBoundingClientRect();

  let windowHeight = document.documentElement.clientHeight;

  // 顶部元素边缘可见吗?
  let topVisible = coords.top > 0 && coords.top < windowHeight;

  // 底部元素边缘可见吗?
  let bottomVisible = coords.bottom < windowHeight && coords.bottom > 0;

  return topVisible || bottomVisible;
}

showVisible() 函数使用通过 isVisible() 实现的可见性检查,来加载可见图像:

function showVisible() {
  for (let img of document.querySelectorAll('img')) {
    let realSrc = img.dataset.src;
    if (!realSrc) continue;

    if (isVisible(img)) {
      img.src = realSrc;
      img.dataset.src = '';
    }
  }
}

showVisible();
window.onscroll = showVisible;

P.S. 此解决方案还有一个 isVisible 的变体,可以“预加载”当前文档滚动上方/下方 1 页内的图像

使用沙箱打开解决方案。

教程路线图

评论

在评论之前先阅读本内容…
  • 如果你发现教程有错误,或者有其他需要修改和提升的地方 — 请 提交一个 GitHub issue 或 pull request,而不是在这评论。
  • 如果你对教程的内容有不理解的地方 — 请详细说明。
  • 使用 <code> 标签插入只有几个词的代码,插入多行代码可以使用 <pre> 标签,对于超过 10 行的代码,建议你使用沙箱(plnkrJSBincodepen…)