图片懒加载

原生JS操作class,element.classList.add(className):添加类名;element.classList.remove(className):删除类名

1.结构示例代码:

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
<!DOCTYPE html>
<html lang="zh">
<head>
<title>图片懒加载</title>
<meta charset="utf-8" />
<meta name="keywords" content="图片懒加载测试" />
<meta name="description" content="多种方法结合进行测试" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="robots" content="all" />
<style type="text/css">
*, html, body {
margin: 0;
padding: 0;
}
html, body {
width: 100vw;
}
img {
width: 100%;
min-height: 300px; // 注意最好指定一个min-height,否则在图片加载进来之前高度值就是0
height: auto;
}
</style>
</head>
<body>
<div class="container">
<ul>
<li><img src="./1.jpg" alt=""></li>
<li><img src="./2.jpg" alt=""></li>
<li><img src="./3.jpg" alt=""></li>
<li><img src="./4.jpg" alt=""></li>
<li><img src="./5.jpg" alt=""></li>
<li><img src="./6.jpg" alt=""></li>
<li><img src="./7.jpg" alt=""></li>
<li><img src="./8.jpg" alt=""></li>
<li><img src="./9.jpg" alt=""></li>
<li><img src="./10.jpg" alt=""></li>
</ul>
</div>
<script type="text/javascript">

</script>
</body>
</html>

假设上面的每一张图片的大小都大概2MB左右,对于上面这样的head中写inline style加上body中写inline script的写法并不会阻塞parse html阶段,DOMContentLoaded事件>onload>FirstPaint事件。

最严重的问题是FirstPaint被触发的时机太晚了,这将造成非常不好的用户体验,因此有必要进行图片懒加载。下面介绍一下实现懒加载的几种方法:

2.API getBoundingClientRect

这个API返回一个DOMRect对象,该对象包含了一组用于描述边框的只读属性-left,top,right,bottom,单位为像素,除了width和height外的属性之外都是相对于视口的左上角位置而言的。

getBoundingClientRect返回值的示例图

2-1. getBoundingClientRect会返回元素盒子的width和height,那么它是什么盒子?

是border-box.举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style type="text/css">
.test {
width: 300px;
height: 300px;
padding: 20px;
border: 10px solid transparent;
}
</style>
<div class="test"></div>
<script>
window.addEventListener('load', () => {
let t = document.getElementsByClassName('.test')[0];
console.log('t.getBoundingClientRect().width', t.getBoundingClientRect().width); // 输出360
});
</script>

同样的对于上面这样的代码,但是对test的样式新增加一行:

1
2
3
4
5
6
7
.test {
box-sizing: border-box;
width: 300px;
height: 300px;
padding: 20px;
border: 10px solid transparent;
}

那么此时利用element.getBoundingClientRect()所获取到的width就是300了。

3.正题,使用getBoundingClientRect这个API来完成懒加载功能:

使用前面的HTML结构,script脚本如下所示:

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
<script type="text/script">
var globalVariable = {};

/** 检查某个DOM元素是否在可视区域内 */
function checkDomInView (dom) {
let result = false;
if (!dom) return result;
let obj = dom.getBoundingClientRect();
// 这里只判断了纵轴方向,并且出于想尽快加载图片的考虑,因此还加了500
if (obj.top >= 0 && obj.bottom <= globalVariable.screenHeight+500) {
result = true;
}
return result;
}

/** 给未加载的图片进行src赋值 */
function setSrc (dom) {
let src = dom.getAttribute('data-src') || '#';
dom.setAttribute('src', src);
dom.classList.remove('lazyloadImg');
}

/** 检查图片是否需要进行懒加载,如果需要的话,进行懒加载 */
function lazyload () {
let imgs = Array.from(document.querySelectorAll('.lazyloadImg'));
if (!imgs || imgs.length == 0) return;
for (let i = 0, len = imgs.length; i < len; i++) {
let img = imgs[i];
if (checkDomInView(img)) { // 图片已经进入视区里面了
setSrc(img);
}
}
}

/** 节流包装方法 */
function throttle (fn) {
let canRun = true;
return function () {
if (!canRun) return;
canRun = false;
setTimeout(function () {
fn && fn();
canRun = true;
}, 500)
}
}

window.addEventListener('DOMContentLoaded', () => {
globalVariable.screenWidth = window.screen.width;
globalVariable.screenHeight = window.screen.height;
lazyload();
document.addEventListener('scroll', throttle(lazyload));
});

window.addEventListener('resize', () => {
globalVariable.screenWidth = window.screen.width;
globalVariable.screenHeight = window.screen.height;
lazyload();
});
</script>

需要注意的地方:最好给img的样式表现设置一个最小高度。上面这个例子只是考虑了纵轴方向上的懒加载,如果需要实现横轴上的话,那么就用left和right去进行比较。

4.使用API:IntersectionObserver

同样是使用上面的HTML结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script type="text/javascript">
/** 检查元素是否出现在可视区域里面 */
function checkDomInView (eleArr) {
if (!eleArr) return;
eleArr.forEach(item => {
if (item.isIntersecting) { // isIntersecting为true或者intersectionRatio>0都能够表示可见
let dom = item.target;
dom.src = dom.dataset.src;
window.oio.unobserve(dom); // 已经达到了监听的目标后那就无需在继续监听了
}
});
}

window.addEventListener('DOMContentLoaded', () => {
let imgs = document.querySelectorAll('.lazyloadImg');
if (!imgs) return;
window.oio = new IntersectionObserver(checkDomInView, { rootMargin: '300px 0px' }); // 增大root的观察区域,以便提前加载图片
imgs.forEach(img => {
window.oio.observe(img);
});
});
</script>

稍微提一下关于IntersectionObserver API,上面使用到的intersectionRatio 表示的意思是指被观察元素和root元素重叠的区域的一个比例值;而isIntersecting就是表示被观察元素相对root元素是否可见。兼容性问题特别不好,因此需要引入polyfill。