这周研究了下目前图片懒加载的所有主流方式,分享下,末尾封装了一个兼容所有方式的图片懒加载插件
图片懒加载的意义:
1.首先它能提升用户的体验,试想一下,如果打开页面的时候就将页面上所有的图片全部获取加载,如果图片数量较大,对于用户来说简直就是灾难,会出现卡顿现象,影响用户体验。
2.有选择性地请求图片,这样能明显减少了服务器的压力和流量,也能够减小浏览器的负担。
原理:
1、将页面中的img标签src指向一张小图片或者src为空,
2、然后定义data-image属性(这个属性可以自定义命名,我常用data-image)属性指向真实的图片。
3、当载入页面时,先把可视区区域内的img标签的data-src属性值赋值给src。
4、然后监听滚动事件,加载用户即将看到的图片
怎么做:
先说结论,四种方式实现图片懒加载:从上到下,也是发展顺序的先后,可以划分为:
- 基于
JS
盒模型实现的懒加载方案(兼容性最好) - 基于
getBoundingClientRect
的进阶方案(ie5以上都能支持) - 终极方案:
IntersectionObserver
(ie不支持,性能最最好) - 未来设想:
img.loading=lazy
(只有chrome76以上支持)
以下只演示核心逻辑:
基于JS
盒模型实现的懒加载方案:
// 加载条件:盒子底边距离BODY距离 < 浏览器底边距离BODY的距离
let winH = window.innerHeight;
let B = lazyImageBox.offsetTop + lazyImageBox.offsetHeight,
A = winH + document.documentElement.scrollTop;
if (B <= A) {
//达到条件,把该img标签data-image属性上的值赋值给src属性
lazyImg(lazyImageBox);
}
基于getBoundingClientRect
的进阶方案
Element.getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置。
let winH = document.documentElement.clientHeight;
let {
bottom
} = lazyImageBox.getBoundingClientRect();
if (bottom <= winH) {
//达到条件,把该img标签data-image属性上的值赋值给src属性
lazyImg(lazyImageBox);
}
基于IntersectionObserver
的终极方案(性能最好)
这个IntersectionObserver
API比较新,这里只说应用,详情看文档
// 实现图片的延迟加载
// IntersectionObserver 监听DOM对象,当DOM元素出现和离开视口的时候触发回调函数
observer = new IntersectionObserver(changes => {
changes.forEach(item => {
let {
//这个属性表示是否出现在视口中,ture/false
isIntersecting,
//目标dom元素
target
} = item;
if (isIntersecting) {
lazyImg(target);
observer.unobserve(target);
}
});
});
let lazyImageBoxs = Array.from(document.querySelectorAll('img'))
lazyImageBoxs.forEach(lazyImageBox => {
observer.observe(lazyImageBox);
});
未来设想:img.loading=lazy
这种用法还在试验阶段,只有chrome76版本以上的浏览器才支持
,实现图片懒加载就一句话,剩下的底层全部帮你做
<img src="xxx.png" alt="" loading="lazy">
图片懒加载插件实现:
基于上述四种方案,封装了一个插件,内部通过能力测试,优先选择性能最佳的去实现图片懒加载
class ImgLazy {
static winH = null
static selector = null
static targetImgDoms = []
static init(selector) {
this.selector = selector
this.winH = document.documentElement.clientHeight;
this.targetImgDoms = Array.from(document.querySelectorAll(selector))
if("loading" in new Image()) {
this.方案四()
return
} else if(typeof IntersectionObserver !== "undefined") {
this.方案三()
return
}
this.lazyFunc()
// onscroll触发的频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能=>我们需要降低onscroll的时候的触发频率 (节流)
window.onscroll = this.throttle(this.lazyFunc, 500).bind(this)
}
static lazyFunc() {
this.targetImgDoms.forEach(img => {
// 已经处理过则不在处理
let isLoad = img.getAttribute('isLoad');
if(isLoad) return;
//getBoundingClientRect判断是否存在
if(!document.body.getBoundingClientRect) {
console.log("方案一")
this.方案一(img)
} else {
console.log("方案二")
this.方案二(img)
}
});
}
static 方案一(img) {
// 加载条件:盒子底边距离距离 < 浏览器底边距离BODY的距离
let B = this.offset(img).top + img.offsetHeight,
A = this.winH + document.documentElement.scrollTop;
if(B <= A) {
this.lazyImg(img);
}
}
static 方案二(img) {
let {
bottom
} = img.getBoundingClientRect();
if(bottom <= this.winH) {
this.lazyImg(img);
}
}
static 方案三() {
let observer = new IntersectionObserver(changes => {
console.log(changes)
changes.forEach(item => {
let {
isIntersecting,
target
} = item;
if(isIntersecting) {
this.lazyImg(target);
observer.unobserve(target);
}
});
});
this.targetImgDoms.forEach(img => {
observer.observe(img);
});
}
static 方案四(img) {
this.targetImgDoms.forEach(img => {
img.setAttribute("loading", "lazy")
this.lazyImg(img)
});
}
static lazyImg(img) {
let trueImg = img.getAttribute('data-image');
img.src = trueImg;
img.onload = function() {
// 图片加载成功
};
img.removeAttribute('data-image');
// 记录当前图片已经处理过了
img.setAttribute('isLoad', 'true');
}
//工具方法:获取元素距离页面顶部的距离(兼容性处理)
static offset(element) {
let parent = element.offsetParent,
top = element.offsetTop,
left = element.offsetLeft;
while(parent) {
if(!/MSIE 8/.test(navigator.userAgent)) {
left += parent.clientLeft;
top += parent.clientTop;
}
left += parent.offsetLeft;
top += parent.offsetTop;
parent = parent.offsetParent;
}
return {
top,
left
};
}
//工具方法:节流
static throttle(func, wait = 500) {
let timer = null,
previous = 0; //记录上一次操作时间
return function anonymous(...params) {
let now = new Date(), //当前操作的时间
remaining = wait - (now - previous);
if(remaining <= 0) {
// 两次间隔时间超过频率:把方法执行即可
clearTimeout(timer);
timer = null;
previous = now;
func.call(this, ...params);
} else if(!timer) {
// 两次间隔时间没有超过频率,说明还没有达到触发标准呢,设置定时器等待即可(还差多久等多久)
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
previous = new Date();
func.call(this, ...params);
}, remaining);
}
};
}
}
插件使用:
需要注意img图片一定要事先撑开,占有一定的空间
<style type="text/css">
div{
width: 400px;
height: 400px;
}
img{
width: 100%;
height: 100%;
}
</style>
<body>
<div><img class="lazy" src="" data-image="2.jpg" /></div>
<div><img class="lazy" src="" data-image="images/1.jpg" /></div>
<div><img class="lazy" src="" data-image="images/2.jpg" /></div>
<div><img class="lazy" src="" data-image="images/3.jpg" /></div>
。。。。。。
</body>
<script>
ImgLazy.init("img.lazy")
</script>
效果: