前言
在我们日常的性能优化里,图片懒加载是一个很常规的操作,它的作用可以理解为用户需要这个资源,才会去加载。市面上有很多现成的库,不过理解其工作原理非常重要,这样可以提升我们的综合能力。
什么是图片懒加载?
在手机或电脑里,屏幕的大小是固定的,这也决定了用户能看到的视图范围有限;假设页面上一个列表有100+张图片,大部分图片不在可视区域内,所以不在可视区域内的图片可以晚点再加载,比如用户滚动就快到看到前加载即可。
懒加载实现原理
把img元素的src属性设置为一个较小的图片链接,把真正要加载的图片链接放到data-src里,然后我们监听元素进入可视区域,再把真正要加载的链接设置到src里。如下:
<!-- 元素进入区域前 -->
<img src="thumbnail.jpg" data-src="original.jpg">
<!-- 元素进入区域后 -->
<img src="original.jpg" data-src="original.jpg">
IntersectionObserver实现监听
官方定义:Intersection Observer API 允许你配置一个回调函数,每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根(root)。通常,您需要关注文档最接近的可滚动祖先元素的交集更改,如果元素不是可滚动元素的后代,则默认为设备视窗。如果要观察相对于根(root)元素的交集,请指定根(root)元素为null。
IntersectionObserver 目前已经被主流的浏览器支持(IE:你们看着我干甚么???),大可放心食用。
用例:
// 创建一个intersectionObserver对象
var intersectionObserver = new IntersectionObserver(callback, options)
// 开始监听某个元素,要监听多个元素需要多次调用。
intersectionObserver.observe(document.getElementById(img))
options是配置参数,主要有3个属性组成
root:所监听对象的具体祖先元素。如果未传入值或值为null时,即代表设备视窗。rootMargin:计算交叉时添加到根(root)边界盒的矩形偏移量,默认为:0px。threshold:一个数组,比如,设置[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
callback回调函数接收一个ertries参数,它是一个数组,每一项代表在这次回调中,被监听的元素可见性发生了变化;每个成员都是一个IntersectionObserverEntry描述了目标元素与root的交叉关系。我们实现图片懒加载暂时只需要关注2个参数。
isIntersecting:返回一个布尔值。如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false。target:监听的元素。
代码实现
<body>
<div>
<img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
<img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
<img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
<img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
<img class="lazyLoad" src="./image/3.jpg" data-src="./image/2.jpg" >
</div>
<script>
var images = document.querySelectorAll('.lazyLoad')
var IntersectionObserver = new IntersectionObserver((ertries) => {
ertries.forEach(entry => {
// 当元素进入可视区内
if (entry.isIntersecting) {
let src = entry.target.getAttribute('data-src')
if ('img' === entry.target.tagName.toLowerCase()) {
if (src) {
entry.target.src = src
}
}
}
})
})
Array.from(images).forEach(image => {
IntersectionObserver.observe(image)
})
</script>
</body>
效果:初始化的时候已经有部分图片在可是区域内了,所以会直接显示需要加载的图片,当我们滑动页面时,注意看下图右侧元素菜单,发现后面2个img元素的src变动了。这就已经简单实现了我们的图片懒加载功能。
图片显示会给你感觉没有懒加载的效果,是因为笔者浏览器有缓存,加载较快导致,但是不影响实际的原理实现。
getBoundingClientRect实现监听
该方法返回一个 DOMRect 对象,是一个包含了一组用于描述边框的只读属性——left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。
我们有了这些数据,只要判断目标元素是否在可视区内就可以了,下面我们先来实现这个方法。
function isInViewport (el) {
const { top, bottom } = el.getBoundingClientRect()
// 为true则代表元素顶部小于窗口高度
const below = top < window.innerHeight ? true : false
// 为true代表元素的底部在可视区内
const above = bottom > 0 ? true : false
return below && above ? true : false
}
上面的代码判断了元素是否在可视区内,那么现在我们只要监听滚动事件,遍历所有的元素,判断它们是否在可视区内就可以了。
var images = document.querySelectorAll('.lazyLoad')
function lazyLoad () {
for (let i = 0; i < images.length; i++) {
let target = images[i]
if (isInViewport(target)) {
let src = target.getAttribute('data-src')
if ('img' === target.tagName.toLowerCase()) {
if (src) {
target.src = src
}
}
}
}
}
// 初始化先执行一次
lazyLoad()
document.addEventListener('scroll', lazyLoad);
document.addEventListener('resize', lazyLoad);
当然,想要优化好再加一个节流函数即可。
lazyload.js源码实现
上面我们已经简单的实现了图片懒加载功能,接下来我们来分析lazyload.js实现的源码,
lazyload.js的实现核心部分只有100多行的代码,下面先看一下核心逻辑。
const defaults = {
src: "data-src", // 代表要真正加载的图片
srcset: "data-srcset", // srcset是响应式的图片,浏览器根据不同手机像素比,加载不同像素的图片
selector: ".lazyload", // 默认需要懒加载元素的选择器
root: null,
rootMargin: "0px",
threshold: 0
};
function LazyLoad(images, options) {
// 默认配置和options的合并
this.settings = extend(defaults, options || {});
// 获取需要懒加载的标签元素
this.images = images || document.querySelectorAll(this.settings.selector);
this.observer = null;
// 初始化
this.init();
}
上面代码是当我们执行new LazyLoad函数后的初始化操作,主要有三部分逻辑:
- 把内置的defaults和传入的options做合并,这个函数不在这里分析。
- 获取需要懒加载的元素列表。
- 初始化IntersectionObserver监听。
核心init函数分析
function init() {
// 如果浏览器不支持IntersectionObserver,则直接加载全部图片。
/* Without observers load everything and bail out early. */
if (!root.IntersectionObserver) {
// 先忽略
this.loadImages();
return;
}
let self = this;
let observerConfig = {
root: this.settings.root,
rootMargin: this.settings.rootMargin,
threshold: [this.settings.threshold]
};
this.observer = new IntersectionObserver(function(entries) {
// 遍历所有发生变化的元素
Array.prototype.forEach.call(entries, function (entry) {
// 当元素进入可视区内
if (entry.isIntersecting) {
// 把该元素取消监听,因为当元素出现在可视区内,就无需再观测了。
self.observer.unobserve(entry.target);
// 获取要加载的图片
let src = entry.target.getAttribute(self.settings.src);
// 获取要加载的响应式的图片
let srcset = entry.target.getAttribute(self.settings.srcset);
if ("img" === entry.target.tagName.toLowerCase()) {
if (src) {
entry.target.src = src;
}
if (srcset) {
entry.target.srcset = srcset;
}
} else {
// 对于其他元素,替换它的背景图
entry.target.style.backgroundImage = "url(" + src + ")";
}
}
});
}, observerConfig);
// 遍历this.images,监听所有目标元素
Array.prototype.forEach.call(this.images, function (image) {
self.observer.observe(image);
});
}
上面的代码首先判断了浏览器是否支持IntersectionObserver方法,如果不支持则执行loadImages方法加载所有图片;接下来初始化 IntersectionObserver 函数;然后遍历所有目标元素,执行 observe 的监听。
当元素进入可视区内时,会触发回调方法,然后再遍历变动的目标元素,修改它们对应的属性即可。
其他函数
function loadImages () {
if (!this.settings) { return; }
let self = this;
Array.prototype.forEach.call(this.images, function (image) {
let src = image.getAttribute(self.settings.src);
let srcset = image.getAttribute(self.settings.srcset);
if ("img" === image.tagName.toLowerCase()) {
if (src) {
image.src = src;
}
if (srcset) {
image.srcset = srcset;
}
} else {
image.style.backgroundImage = "url('" + src + "')";
}
});
}
function destroy () {
if (!this.settings) { return; }
// 关闭观察
this.observer.disconnect();
this.settings = null;
}
loadImages 函数与上面分析的基本一致,destroy函数用来摧毁观察。
注意
- 也可以使用
imgEle.offsetTop < window.innerHeight + document.body.scrollTop,观察图片是否进入可视区内实现,不过方法比较low,就没有去实现了。 - 监听浏览器scroll事件去实现懒加载,是无法在 better-scroll 这种库使用的。但是用 IntersectionObserver 实现的这种方式是不受影响。
感谢观看我的文章,如果对您有帮助,请点个赞👍~