图片懒加载
# 图片的懒加载
# 原理
图片懒加载技术主要通过监听图片资源容器是否出现在视口区域内,来决定图片资源是否被加载。
那么实现图片懒加载技术的核心就是如何判断元素处于视口区域之内。
# 利用scroll
事件
思路
- 给目标元素指定一张占位图,将真实的图片链接存储在自定义属性中,通常是
data-src
; - 监听与用户滚动行为相关的
scroll
事件; - 在
scroll
事件处理程序中利用Element.getBoundingClientRect()
方法判断目标元素与视口的交叉状态; - 当目标元素与视口的交叉状态大于
0
时,将真实的图片链接赋给目标元素src
属性或者backgroundImage
属性。 - 利用函数节流降低回流
let imgs = document.getElementsByTagName("img"),
count = 0;
// 首次加载
lazyLoad();
// 通过监听 scroll 事件来判断图片是否到达视口,别忘了防抖节流
window.addEventListener("scroll", throttle(lazyLoad, 160));
function lazyLoad() {
let viewHeight = document.documentElement.window.innerHeight; //视口高度
//滚动条卷去的高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for (let i = count; i < imgs.length; i++) {
// 元素现在已经出现在视口中
if (imgs[i].offsetTop < scrollTop + viewHeight) {
if (imgs[i].getAttribute("src") !== "default.jpg") continue;
imgs[i].src = imgs[i].getAttribute("data-src");
count++;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# getBoundingClientRect()
方法
JavaScript
提供 Element.getBoundingClientRect()
方法返回元素的大小以及相对于视口的位置信息,接下来会用到返回对象的四个属性:
top
和left
是目标元素左上角坐标与网页左上角坐标的偏移值;width
和height
是目标元素自身的宽度和高度。
再结合视口的高度和宽度,即可判断元素是否出现在视口区域内:
function isElementInViewport (el) {
const { top, height, left, width } = el.getBoundingClientRect()
const w = window.innerWidth || document.documentElement.clientWidth
const h = window.innerHeight || document.documentElement.clientHeight
return (
top <= h &&
(top + height) >= 0 &&
left <= w &&
(left + width) >= 0
)
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
function LazyLoad(el, options) {
if (!(this instanceof LazyLoad)) {
return new LazyLoad(el);
}
this.setting = Object.assign(
{},
{ src: "data-src", srcset: "data-srcset", selector: ".lazyload" },
options
);
if (typeof el === "string") {
el = document.querySelectorAll(el);
}
this.images = Array.from(el);
this.listener = this.loadImage();
this.listener();
this.initEvent();
}
LazyLoad.prototype = {
loadImage() {
return throttle(function () {
let startIndex = 0;
while (startIndex < this.images.length) {
const image = this.images[startIndex];
if (isElementInViewport(image)) {
const src = image.getAttribute(this.setting.src);
const srcset = image.getAttribute(this.setting.srcset);
if (image.tagName.toLowerCase() === "img") {
if (src) {
image.src = src;
}
if (srcset) {
image.srcset = srcset;
}
} else {
image.style.backgroundImage = `url(${src})`;
}
this.images.splice(startIndex, 1);
continue;
}
startIndex++;
}
if (!this.images.length) {
this.destroy();
}
}).bind(this);
},
initEvent() {
window.addEventListener("scroll", this.listener, false);
},
destroy() {
window.removeEventListener("scroll", this.listener, false);
this.images = null;
this.listener = null;
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
简单版
function lazyload() {
for(let i = count; i <num; i++) {
// 元素现在已经出现在视口中
if(img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 利用IntersectionObserver
可以异步监听目标元素与其祖先或视窗的交叉状态,注意这个接口是异步的,它不随着目标元素的滚动同步触发,所以它并不会影响页面的滚动性能
options
:
配置项中的参数有以下三个:
root
:所监听对象的具体祖先元素,默认是viewport
rootMargin
:计算交叉状态时,将margin
附加到祖先元素上,从而有效的扩大或者缩小祖先元素判定区域;threshold
:设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数
回调函数
IntersectionObserver
实例执行回调函数时,会传递一个包含 IntersectionObserverEntry
对象的数组,该对象一共有七大属性:
time
:返回一个记录从IntersectionObserver
的时间原点到交叉被触发的时间的时间戳;target
:目标元素;rootBounds
:祖先元素的矩形区域信息;boundingClientRect
:目标元素的矩形区域信息,与前面提到的Element.getBoundingClientRect()
方法效果一致;intersectionRect
:祖先元素与目标元素相交区域信息;intersectionRatio
:返回intersectionRect
与boundingClientRect
的比例值;isIntersecting
:目标元素是否与祖先元素相交
在此之前,还需要了解 IntersectionObserver
实例方法:
observe
:开始监听一个目标元素;unobserve
:停止监听特定的元素;disconnect
:使IntersectionObserver
对象停止监听工作;takeRecords
:为所有监听目标返回一个IntersectionObserverEntry
对象数组并且停止监听这些目标。
function LazyLoad (images, options = {}) {
if (!(this instanceof LazyLoad)) {
return new LazyLoad(images, options)
}
this.setting = Object.assign({}, { src: 'data-src', srcset: 'data-srcset', selector: '.lazyload' }, options)
this.images = images || document.querySelectorAll(this.setting.selector)
this.observer = null
this.init()
}
LazyLoad.prototype.init = function () {
let self = this
let observerConfig = {
root: null,
rootMargin: '0px',
threshold: [0]
}
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const target = entry.target
if (entry.intersectionRatio > 0) {
this.observer.unobserve(target)
const src = target.getAttribute(this.setting.src)
const srcset = target.getAttribute(this.setting.srcset)
if ('img' === target.tagName.toLowerCase()) {
if (src) {
target.src = src
}
if (srcset) {
target.srcset = srcset
}
} else {
target.style.backgroundImage = `url(${src})`
}
}
})
}, observerConfig)
this.images.forEach(image => this.observer.observe(image))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 或者简单一点
let imgs = document.getElementsByTagName("img")
const observer = new IntersectionObserver(changes => {
for(let i=0, len=imgs.length; i<len; i++) {
let img = imgs[i]
// 通过这个属性判断是否在视口中,返回 boolean 值
if(img.isIntersecting) {
const imgElement = img.target
imgElement.src = imgElement.getAttribute("data-src")
observer.unobserve(imgElement) // 解除观察
}
}
})
Array.from(imgs).forEach(item => observer.observe(item)) // 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上次更新: 2022/03/10, 14:51:31