原文链接:http://javascript.info/onscroll,translate with ❤️ by zhangbao.
滚动事件允许我们在滚动页面或元素时做出响应,我们可以以此为契机,做很多事情。
例如:
根据文档现在的滚动位置,展示/隐藏相关控件和信息。
当滚动到页面底部时,加载更多数据。
下面是显示当前滚动偏移量的小函数:
window.addEventListener('scroll', function () {
document.getElementById('showScroll').innerHTML = pageYOffset + 'px';
});
scroll
事件可以绑定在 window
和其他所有的可滚动元素上。
阻止滚动
我们怎样阻止滚动呢?如果在 onscroll
事件处理器中使用 event.preventDefault()
阻止滚动是行不通的,因为这个事件是在滚动之后触发的。
但我们可以在引起滚动的事件的事件处理器中使用 event.preventDefault()
阻止滚动发生。
例如:
wheel
事件——滚动鼠标滚轮时(一个“滚动”触摸板动作也会生成它)。keydown
事件—— 按下pageUp
或pageDown
按键时。
有些时候这有些帮助。但滚动方式有多种,很难处理所有情况。最可靠的方式是使用 CSS 阻止滚动,比如使用 overflow
属性。
下面布置了一些任务,供你解决并体会 onscroll 在应用程序中的使用。
练习题
问题
一、无限滚动页面
创建一个可以无限滚动的页面。当用户滚动到页面底部的时候,自动在尾部添加时间的文本表示,这样用户就可以进一步滚动页面了。
滚动时,有两个重要的特性点:
滚动是“灵活的”。在某些浏览器/设备中,我们可以稍微超出文档开始或结束的范围滚动(显示下面的空白区域,然后文档将自动“弹回”到正常状态)。
滚动是不精确的。当我们滚动到页面末尾时,我们可能实际上距离真正的文档底部有
0~50px
的距离。
因此,“滚动到底部”表示用户滚动到距离文档底部不超过 100px
的距离。
P.S. 在实际场景下,通过显示的是“更多消息”或“更多商品”之类的。
二、“回到顶部”按钮
创建一个“回到顶部”按钮帮助页面滚动。
它的行为如下:
当页面滚动没有超过一个窗口高度的时候,这个按钮是隐藏的。
当页面滚动超过一个窗口高度了,在左上角显示“回到顶部”按钮。当页面滚动回去了,按钮消失。
点击按钮的时候,页面滚动到顶部。
三、图片懒加载
我假设我们有一个速度较慢的客户端,希望节省他们的移动通信流量。
为了这目的,我们不准备立即显示图片,而是先代之以一个占位图片,想这样:
<img src="placeholder.svg" width="128" height="128" data-src="real.jpg">
因此一开始,所有图片都引用 palceholder.svg
。当页面滚动到可以看见图片地方的时候——我们将 data-src
属性值替换 src
,这时才加载图片。
点击滚动查看图片懒加载效果。
要求:
当页面加载的时候,当前视口内的看见图片应立即显示,而不是在滚动之后。
一些图片可能无需懒加载效果,它们没有
data-src
属性,我们无需处理它们。图片一旦加载,当再次滚动进入/离开的时候,都不要再重新加载了。
P.S. 如果可以的话,可以提供一个更加高级的解决方案——“预加载”当前位置的下方/之后一页的图片。
P.P.S 只需处理垂直滚动就可以了,不用管水平滚动的情况。
答案
一、无限滚动页面
解决方案的核心就是当我们滚动到页面底部的时候,有一个函数用来向页面中添加更多日期(实际情景下可能是其他内容)。
我们理解调用这个函数,并且添加一个 window.onscroll
处理程序。
最重要的问题是:“我们怎么探测页面已经滚动到底部了”。
这里就要用到窗口坐标了。
文档是表现(包含)在 <html>
标签中的,也就是 document.documentElement
。
我们可以使用 document.documentElement.getBoundingClientRect()
获取文档在窗口坐标中的相对位置。其中的 .bottom
属性表示文档底部距离窗口顶部的距离。
例如,如果整个文档的高度是 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
坐标值限制在最小一个窗口的高度,因为我们无法再向下滚动了。
窗口高度使用 document.documentElement.clientHeight
获得。
我们想要保证窗口底部和文档底部之间至少 100px 远的距离,也就是窗口顶部距离文档底部的距离至少是 (document.documentElement.clientHeight + 100) px
这么远。
下面是函数实现:
function populate() {
while(true) {
// 获取文档底部到窗口顶部的距离
let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom;
// 如果这个距离大于 一个窗口高度 + 100px,说明我们还没有到达页面底部
// 无需加载更多数据
if (windowRelativeBottom > document.documentElement.clientHeight + 100) break;
// 否则加载更多数据(加载数后,while 循环继续下次迭代,因为添加了一个内容让距离变大了,下次迭代时就 break 了)
document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
}
}
这里是完整代码:
function populate() {
while(true) {
let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom;
if (windowRelativeBottom > document.documentElement.clientHeight + 100) break;
document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`);
}
}
window.addEventListener('scroll', populate);
populate(); // 初始化
在线查看。
二、“回到顶部”按钮
<!DOCTYPE HTML>
<html>
<head>
<style>
body,
html {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
}
#matrix {
width: 400px;
margin: auto;
overflow: auto;
text-align: justify;
}
#arrowTop {
height: 9px;
width: 14px;
color: green;
position: fixed;
top: 10px;
left: 10px;
cursor: pointer;
}
#arrowTop::before {
content: '▲';
}
</style>
<meta charset="utf-8">
</head>
<body>
<div id="matrix">
<script>
for (let i = 0; i < 2000; i++) document.writeln(i)
</script>
</div>
<div id="arrowTop" hidden></div>
<script>
arrowTop.onclick = function() {
window.scrollTo(window.pageXOffset, 0);
// 使用 scrollTo 会触发 scroll 事件,因此箭头会自动隐藏
};
window.addEventListener('scroll', function() {
arrowTop.hidden = (window.pageYOffset < document.documentElement.clientHeight);
});
</script>
</body>
</html>
在线查看。
三、图片懒加载
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 > 0 && coords.bottom < windowHeight;
return topVisible || bottomVisible;
}
showVisible();
window.onscroll = showVisible;
对在可视区区域的图片可以这样处理:将 img.dataset.src
的属性值赋给 img.src
(如果没做的话)。
P.S. 解决方案有一个变体 isVisible
——“预加载”下面/上面一页里的图片(窗口高度使用 document.documentElement.clientHeight
获取)。
/**
* 检查元素是否可见(在页面的可视区范围之内)
* 元素的顶部或者底部在可视区范围都认为是可见的
*/
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;
}
/**
一个变体:检查当前可视区上&下一页的图片,并提前加载起来!
function isVisible(elem) {
let coords = elem.getBoundingClientRect();
let windowHeight = document.documentElement.clientHeight;
let extendedTop = -windowHeight;
let extendedBottom = 2 * windowHeight;
// 顶部可见 || 底部可见
let topVisible = coords.top > extendedTop && coords.top < extendedBottom;
let bottomVisible = coords.bottom > extendedTop && coords.bottom < extendedBottom;
return topVisible || bottomVisible;
}
*/
function showVisible() {
for (let img of document.querySelectorAll('img')) {
let realSrc = img.dataset.src;
// 加载后的图片不再处理
if (!realSrc) continue;
if (isVisible(img)) {
// 禁用缓存
// 这行代码在生产环境下请删除
realSrc += '?nocache=' + Math.random();
img.src = realSrc;
img.dataset.src = '';
}
}
}
window.addEventListener('scroll', showVisible);
showVisible();
(完)