图片懒加载的实现
javascript 懒加载 性能优化 前端
图片懒加载,就是通过代码控制,只加载可视区域内的图片,减少请求,一般有两种实现方式:
1.监听滚动
- 给目标元素指定一张占位图,将真实的图片链接存储在自定义属性中,通常是 data-src;
- 监听与用户滚动行为相关的 scroll 事件;
- 在 scroll 事件处理程序中利用 Element.getBoundingClientRect() 方法判断目标元素与视口的交叉状态;
- 当目标元素与视口的交叉状态大于 0 时,将真实的图片链接赋给目标元素 src 属性或者 backgroundImage 属性。
1.1 判断元素是否在可视窗口内
isElementInViewport.js
function isElementInViewport(el) {
// top, left, bottom 是目标元素左上角坐标与网页左上角坐标的偏移值
// width 和 height 是目标元素自身的宽度和高度。
const { top, bottom, left, width } = el.getBoundingClientRect();
// 再结合视口的高度和宽度,即可判断元素是否出现在视口区域内:
const w = window.innerWidth || document.documentElement.clientWidth;
const h = window.innerHeight || document.documentElement.clientHeight;
console.log(top, bottom);
return top <= h && bottom >= 0 && left <= w && left + width >= 0;
}
1.2 节流函数
throttle.js
因为 scroll 会不停触发,所以需要节流,这里有两种实现方式:
// setTimeout + 闭包
function throttle(fn, interval = 500) {
let timer = null;
let firstTime = true;
return function (...args) {
if (firstTime) {
// 第一次加载
fn.apply(this, args);
return (firstTime = false);
}
if (timer) {
// 定时器正在执行中,跳过
return;
}
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
fn.apply(this, args);
}, interval);
};
}
// requestAnimationFrame
function throttle(fn) {
let lock = false;
return function (...args) {
if (lock) return;
lock = true;
window.requestAnimationFrame(() => {
fn.apply(this, args);
lock = false;
});
};
}
1.3 具体实现类
lazyloadScroll.js
function LazyLoad(el, options) {
if (!(this instanceof LazyLoad)) {
return new LazyLoad(el);
}
this.setting = Object.assign(
{},
{ src: "data-src", srcset: "data-srcset" },
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.4 html 示例
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
img {
display: block;
margin: auto;
height: 200px;
}
</style>
<script src="throttle.js"></script>
<script src="isElementInViewport.js"></script>
<script src="lazyloadScroll.js"></script>
</head>
<body>
<img
data-src="https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=1684532727,1424929765&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=4049022245,514596079&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=1472391233,99561733&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=3363566985,99735131&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=3219906533,2982923681&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=1771079238,1156389364&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=3038223445,2416689412&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=1839135015,723795615&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=1275095085,1961143463&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=63249423,2260265143&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=4187423482,206374644&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=2243264347,2203972402&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=530426417,2082848644&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=3344911223,3409692090&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=204971088,1987351322&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
</body>
<script>
const lazyLoader = new LazyLoad("img");
</script>
</html>
2. IntersectionObserver
IntersectionObserver 接口可以异步监听目标元素与其祖先或视窗的交叉状态,这个接口是异步的,它不随着目标元素的滚动同步触发,所以它并不会影响页面的滚动性能。
IntersectionObserver 实例方法:
- observe:开始监听一个目标元素;
- unobserve:停止监听特定的元素;
- disconnect:使 IntersectionObserver 对象停止监听工作;
- takeRecords:为所有监听目标返回一个 IntersectionObserverEntry 对象数组并且停止监听这些目标。
IntersectionObserver 配置项
- root:所监听对象的具体祖先元素,默认是 viewport ;
- rootMargin:计算交叉状态时,将 margin 附加到祖先元素上,从而有效的扩大或者缩小祖先元素判定区域;
- threshold:设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数。
IntersectionObserver 回调函数
IntersectionObserver 实例执行回调函数时,会传递一个包含 IntersectionObserverEntry 对象的数组,该对象一共有七大属性:
- time:返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳;
- target:目标元素;
- rootBounds:祖先元素的矩形区域信息;
- boundingClientRect:目标元素的矩形区域信息,与前面提到的 Element.getBoundingClientRect() 方法效果一致;
- intersectionRect:祖先元素与目标元素相交区域信息;
- intersectionRatio:返回 intersectionRect 与 boundingClientRect 的比例值;
- isIntersecting:目标元素是否与祖先元素相交。
2.1 具体实现类
LazyLoadIO.js
function LazyLoad(el, options = {}) {
if (!(this instanceof LazyLoad)) {
return new LazyLoad(images, options);
}
this.setting = Object.assign(
{},
{ src: "data-src", srcset: "data-srcset", selector: ".lazyload" },
options
);
if (typeof el === "string") {
el = document.querySelectorAll(el);
}
this.images = el || document.querySelectorAll(this.setting.selector);
this.observer = null;
this.init();
}
LazyLoad.prototype.init = function () {
let observerConfig = {
root: null, // 默认是viewport视窗
rootMargin: "0px", // 计算交叉状态时,将 margin 附加到祖先元素上,从而有效的扩大或者缩小祖先元素判定区域
threshold: [0], // 设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数
};
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const target = entry.target;
// 当target元素与窗口相交时
if (entry.isIntersecting) {
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));
};
2.2 html 示例
跟方法一使用方式一样,引用的 js 换一下就可以
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
img {
display: block;
margin: auto;
height: 200px;
}
</style>
<!--这里引用的js换下就可以-->
<script src="lazyloadIO.js"></script>
</head>
<body>
<img
data-src="https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=1684532727,1424929765&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=4049022245,514596079&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=1472391233,99561733&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=3363566985,99735131&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=3219906533,2982923681&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=1771079238,1156389364&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=3038223445,2416689412&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=1839135015,723795615&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=1275095085,1961143463&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=63249423,2260265143&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=4187423482,206374644&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=2243264347,2203972402&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800"
alt=""
/>
<img
data-src="https://img0.baidu.com/it/u=530426417,2082848644&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
alt=""
/>
<img
data-src="https://img2.baidu.com/it/u=3344911223,3409692090&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313"
alt=""
/>
<img
data-src="https://img1.baidu.com/it/u=204971088,1987351322&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
alt=""
/>
</body>
<script>
const lazyLoader = new LazyLoad("img");
</script>
</html>