前言

滚动加载和分页在前端其实用的场景挺多的,都是避免一次性请求太多数据导致网站加载数据过慢,这次来做做无限滚动加载,具体滚动加载和分页的区别可以看这篇文章

无限滚动原理

其实无限滚动的原理很简单,用户滚动,当滚动条到底的时候就加载,所以只需要知道滚动条和底部的距离,就可以完成了,当然其中还有一些细节,我们介绍几种方法。

第一种方法

首先,浏览器提供了很多属性,可以给我们计算位置:

scrollHeight

  • 读写:只读
  • 描述:包括 overflow 样式属性导致的视图中不可见内容,没有垂直滚动条的情况下,scrollHeight 值与元素视图填充所有内容所需要的最小值 clientHeight 相同。包括元素的 padding ,但不包括元素的 margin.

scrollTop

  • 读写:可读可写
  • 描述:这个Element.scrollTop 属性可以设置或者获取一个元素距离他容器顶部的像素距离。一个元素的 scrollTop 是可以去计算出这个元素距离它容器顶部的可见高度。当一个元素的容器没有产生垂直方向的滚动条,那它的 scrollTop 的值默认为0.

clientHeight

  • 读写:只读
  • 描述:对于没有定义CSS或者内联布局盒子的元素为0,否则,它是元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。
    clientHeight 可以通过 CSS height + CSS padding - 水平滚动条高度 (如果存在)来计算.

使用公式

如果元素滚动到底,下面等式返回 true,没有则返回 false.
element.scrollHeight - element.scrollTop === element.clientHeight

局限

这种方法不能确定浏览器的滚动条是否滚到底,也就是说通常我们浏览器右边的滚动条的滚动到底,这个公式没用:

1
2
3
4
5
6
7
8
function scroll(){
var pageHeight = document.body.scrollHeight
var viewportHeight = document.body.clientHeight;
var scrollHeight = document.body.scrollTop;
console.log(pageHeight - scrollHeight === viewportHeight );
}

setInterval(lowEnough,1000); // 不管滚动条有没有到底都不会变

起作用的一般是 textarea 这些标签内部的滚动条。
具体例子

第二种方法

这种其实也是用到浏览器提供的属性:

offsetHeight

  • 读写:只读
  • 描述:它返回该元素的像素高度,高度包含该元素的垂直内边距和边框,且是一个整数。

window.innerHeight

  • 读写:只读
  • 描述:浏览器窗口的视口(viewport)高度(以像素为单位),如果存在水平滚动条,则包括它。

document.documentElement

  • 读写:只读
  • 描述:使用这个只读属性能很方便的获取到任意文档的根元素。 HTML 文档通常包含一个子节点,可能在它前面还有个DOCTYPE声明。XML 文档通常包含多个子节点:根元素,DOCTYPE声明,和 [processing instructions](https://developer.mozilla.org/zh-CN/docs/DOM/ProcessingInstruction)。 所以你应该使用document.documentElement `来获取根元素, 而不是document.firstChild

window.pageYOffset

  • 读写:只读
  • 描述:pageYOffsetscrollY 的别名。
    返回文档在垂直方向已滚动的像素值。

具体使用

1
2
3
4
5
6
7
8
9
10
function lowEnough(){
var pageHeight = Math.max(document.body.scrollHeight,document.body.offsetHeight);
var viewportHeight = window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight || 0;
var scrollTop = window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop || 0;
return pageHeight - viewportHeight - scrollTop < 20; // 通过 真实内容高度 - 视窗高度 - 上面隐藏的高度 < 20,作为加载的触发条件
}

用这个函数就能够监控整个页面的滚动。

第三种方法

看看一个api:

getBoundingClientRect

返回一个 DOMRect 对象,该对象限定了选定的文档对象的内容,该方法返回了一个矩形,这个矩形包围了该文档对象中所有元素的边界矩形集合。

使用场景

在长列表最后埋一个空元素,然后判断这个空元素是否可见,如果可见,则加载,掘金就是用这种方案

最后一个是空元素

使用公式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function checkIsTotalVisible (element) {
const rect = element.getBoundingClientRect()
const {
top,
left,
bottom,
right
} = rect
const isTotalVisible = (
top >= 0
&&
left >= 0
&&
bottom < document.documentElement.clientHeight
&&
right < document.documentElement.clientWidth
)
return isTotalVisible
}

第四种方法

也是和第三种一样,判断元素是否出现来加载,用到一个api:

IntersectionObserverEntry

这里就引用 阮一峰的文章

1
var io = new IntersectionObserver(callback, option);

上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点。

1
2
3
4
5
6
7
8
// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

1
2
io.observe(elementA);
io.observe(elementB);

callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback

callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

1
2
3
4
5
6

var io = new IntersectionObserver(
entries => {
console.log(entries);
}
);

上面代码中,回调函数采用的是箭头函数的写法。callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

IntersectionObserverEntry 对象

IntersectionObserverEntry对象提供目标元素的信息,一共有六个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}

每个属性的含义如下。

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素,是一个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
image
image

上图中,灰色的水平方框代表视口,深红色的区域代表四个被观察的目标元素。它们各自的intersectionRatio图中都已经注明。

实例:无限滚动

无限滚动(infinite scroll)的实现也很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13

var intersectionObserver = new IntersectionObserver(
function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
});

// 开始观察
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);

无限滚动时,最好在页面底部有一个页尾栏(又称sentinels)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。这样做的好处是,不需要再一次调用observe()方法,现有的IntersectionObserver可以保持使用。

引用:
JavaScript实现列表无限加载
10行代码实现页面无限滚动
MDN