目录
需求
思路分析
判断图片是否在可视区域内
图片设置
父元素设置
滚动事件回调函数
问题
优化
只记录还没有加载的图片
节流
实现效果
总结
代码
工作的时候暂时没有活,所以写个懒加载的踩坑记录。需求就是最平常的图片懒加载,使用一个数组存储所有图片的地址,等到图片出现在可视区域内的时候才去设置其src属性,这样图片才会加载。
这个是最关键的地方,只有正确判断图片中可视区域中才可以正确加载图片。img是图片,parent是其容器,我的具体判断公式如下:
img.offsetHeight + img.offsetTop > parent.scrollTop && img.offsetTop < parent.scrollTop + parent.clientHeight
公式分为两部分。前半部分是判断图片是否能够进入可视区域,后半部分是判断图片是否离开可视区域。其逻辑可以画图捋一下,不再细说。
对于一个图片,我们一开始不想加载它,所以用background设置背景图片作为占位。并且,我们还要把它的地址存到它的身上,以便于加载。可以考虑存在元素的dataset中,比如img.dataset.src。这样,对于每个图片,应该做如下设置:
![]()
这是使用原生HTML写的,如果采用vue等框架可以直接遍历数组地址产生img。还需要给img标签指定类名,我写为lazy。然后给lazy类设置统一样式:
.lazy {
width: 400px;
height: 400px;
background-image: url("https://cn.vuejs.org/images/logo.svg");
background-repeat: no-repeat;
background-size: contain;
margin-bottom: 50px;
}
这里用背景实现占位,background-image就是默认图片。后面如果给图片设置了src,就可以盖住背景了。
我简单使用一个类名为container的div包裹图片:
然后给container类设置样式:
.container {
background-color: floralwhite;
width: 60vw;
height: 100%;
overflow-y: auto;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
这样,所有图片就是水平居中排列的了。
要实现懒加载,必须监听滚动事件,在监听函数中判断哪些图片在可视区域,然后加载它。刚刚已经实现了判断图片是否在可视区域的逻辑,现在封装滚动事件的回调函数:
// 判断图片是不是在可视区域内
// parent:父元素 img:图片元素
function ifInview(parent, img) {
return img.offsetHeight + img.offsetTop > parent.scrollTop && img.offsetTop < parent.scrollTop + parent.clientHeight
}
// 滚动事件的回调函数
function load() {
console.log('load')
let parent = document.getElementsByClassName('container')[0];
let img_list = document.getElementsByClassName('lazy');
for (let i = 0; i < arr.length; i++) {
let img = img_list[i];
if (ifInview(parent, img)) {
img.src = img.dataset.src;
}
}
}
然后给父元素绑定scroll事件监听即可:
let container = document.getElementsByClassName('container')[0]
container.addEventListener('scroll', load);
大功告成。
写完这些代码发现,滚动的时候并没有触发scroll事件的回调函数。这是为什么?
检查代码,发现问题:在container中,给它设置高度为100%。container的高度继承自body,body继承自html。html中包含了6个图片,因此它就具有6个图片的高度,因此container就具有6个图片的高度。因此,对于container来讲,它根本没有溢出,所以它的overflow-y:auto根本就没有起作用!!!
解决方法就是,给container设置一个固定高度,我设为100vh。
设置后发现仍然不能触发。再细读,发现我对container使用了flex布局,这样就导致子元素的高度被限制在父元素内,无法超过父元素的高度,自然也就无法触发scroll事件。因此,需要放弃flex布局。我将img设置为块元素,然后让它们居中显示,效果一样。
改动后的代码:
.container {
background-color: floralwhite;
width: 60vw;
height: 100vh;
overflow-y: scroll;
margin: 0 auto;
}
.lazy {
width: 400px;
height: 400px;
display: block;
background-image: url("https://cn.vuejs.org/images/logo.svg");
background-repeat: no-repeat;
background-size: contain;
background-position: center;
margin: 20px auto;
}
再次尝试,就成功了。
基本功能实现后,可以对它进行优化。
首先考虑一点,每次滚动事件都会调用load函数。但是,如果现在所有图片都已经加载完毕了,就没有必要调用它了。所以,需要记录目前还没有加载的图片,下次调用只需要遍历它们即可。为此,我把获取到的子元素列表存在load函数外面,然后在每次load函数调用的时候,移除掉加载完成的图片。要实现移除,我使用Array.from将子元素伪数组转化为数组,这样就可以调用splice方法了。
改进后的代码如下:
// 获取父元素和所有子元素
let parent = document.getElementsByClassName('container')[0];
// 转换为数组
let img_list = Array.from(document.getElementsByClassName('lazy'));
// 滚动事件的回调函数
function load() {
if (!img_list.length) {
return;
}
for (let i = 0; i < img_list.length; i++) {
let img = img_list[i];
if (ifInview(parent, img)) {
img.src = img.dataset.src;
img_list.splice(i, 1)
}
}
}
这样,每次加载一张图片,就会把它从子元素列表中移除。当img_list的长度为0时,会直接返回。
现在还有一个问题,就是每次滚动,均会触发load函数,过于频繁。因此,可以采用防抖或节流的方式,减少调用的次数。这里我采用了节流。具体思路就是,封装一个实现节流函数的方法,对load进行节流包装,然后把scroll事件的回调函数设置为这个节流函数即可。
实现如下:
// 节流函数
function throttle(func, delay) {
let last = Date.now();
return function () {
let now = Date.now();
if (now - last < delay) {
return;
}
last = now;
func.apply(window);
}
}
// 获取节流后的回调函数
let throttle_load = throttle(load, 500);
let container = document.getElementsByClassName('container')[0]
// 将节流回调设置为scroll事件的回调函数
container.addEventListener('scroll', throttle_load);
设置每500毫秒触发一次,实现节流效果。
这里我使用了20张图片,效果更明显些。
懒加载演示
在完成懒加载时,需要注意这么几点:
1、图片的父元素高度不能设置为100%
2、所有子元素的高度一定要超过父元素,触发溢出
3、图片是否在可视区域内的判断要准确
4、节流等优化手段
附上简陋的代码。因为是实习闲暇写的,所以比较简单,没有什么复杂的逻辑,望批评指正。
Document