【JavaScript】图片的懒加载
文章目录
1. 懒加载
在一个很长很长的页面里,只渲染可视区域里的内容对于性能提升非常有用。图片懒加载就是这样一种技术,我们可以在图片进入可视区域之后再去请求/渲染。
这需要一个技巧,就是暂时不给 <img>
标签的 src
属性赋值,需要加载时再赋值。
<img src="" data-src="xxx.jpg">
我们利用[data-src]
来标记好图片来源,等图片进入可视区域时,将其赋值给 src
即可。
img.src = img.dataset.src;
于是,问题的难点成了如何判断 图片有没有进入可视区域。
2. 利用scroll
事件
显然,我们需要滚动触发加载。
利用HTMLElement.offsetTop
html
的 clientHeight
是当前可视区域(viewport
)的高度。
document.documentElement.clientHeight
html
的 scrollTop
是当前滚动条距离 html
顶部的距离。
document.documentElement.scrollTop
向下滚动时,通过判断 图片距离 html
顶部的距离 小于了 上面二者的和,则说明进入了可视区域(或者在可视区域上方);总之不在下方(没滚动到的地方)。
所以问题就成了怎么获取 图片距离 html
顶部的距离。
元素ele
距离html
顶部的距离,可以通过 ele.offsetTop
属性获取,但是该属性是相对于 ele.offsetParent
的;而我们就需要一直递归到html/body
为止。
offsetParent
returnsnull
in the following situations:
- The element or its parent element has the
display
property set tonone
.- The element has the
position
property set tofixed
(firefox returns<body>
).- The element is
<body>
or<html>
.
根据描述,当递归到 body
时会返回 null
function getOffsetTopFromRoot(ele) {
let top = ele.offsetTop;
while (ele = ele.offsetParent) {
top += ele.offsetTop;
}
return top;
}
所以得到进入视口的条件为 getOffsetTopFromRoot(img) <= clientHeight + scrollTop
const images = document.querySelectorAll('img');
const loadImage = (img) => img.src ||= img.dataset.src;
function lazyLoadImage(img) {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
if (getOffsetTopFromRoot(img) <= clientHeight + scrollTop) {
loadImage(img);
}
}
function loadImages() {
images.forEach(img => {
lazyLoadImage(img);
})
}
loadImages();
window.addEventListener('scroll', loadImages);
实现了,但是,这,也太麻烦了吧。
利用Element.getBoundingClientRect()
其实。浏览器提供了一个获取元素相对于视口的距离信息的API
。
Element.getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置
既然如此,我们很容易得到判断条件为 img.getBoundingClientRect().top <= clientHeight
,上述代码替换掉条件即可。
节流优化
针对事件触发,可以做节流优化。
function throttle(func, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
func();
timer = null;
}, delay);
}
}
}
const loadImages = throttle(() => {
images.forEach(img => {
lazyLoadImage(img);
})
}, 500);
3. 利用IntersectionObserver
上述方法存在性能问题,怪异且不优雅。于是有了专门的API
。
Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。
IntersectionObserver
甚至为我们提供好了 entry.intersectionRatio
来获取出现在视口里的比例,在0-1
之间,于是我们只需要判断 entry.intersectionRatio > 0
即可
const images = document.querySelectorAll('img');
const loadImage = (img) => img.src ||= img.dataset.src;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
loadImage(entry.target);
}
})
});
images.forEach(img => {
observer.observe(img);
})