前言
完整代码,请移步:CQ-engineer/juejinBlog/imgLazyLoad
之前笔者,也了解过图片懒加载,但也仅仅是了解,写过一些简单的代码,但是没有真正用到项目当中,这次正好项目当中用到了,而且结合一些学习资料,那么我就来根据各种方式方法来实现一遍吧
首先看看之前的代码
// 实现懒加载
// <ul>
// <li><img src="./imgs/default.png" data="./imgs/1.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/2.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/3.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/4.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/5.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/6.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/7.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/8.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/9.png" alt=""></li>
// <li><img src="./imgs/default.png" data="./imgs/10.png" alt=""></li>
// </ul>
let imgs = document.querySelectorAll('img')
// 可视区高度
let clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
/*function lazyLoad () {
// 滚动卷去的高度
let scrollTop = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
for (let i = 0; i < imgs.length; i ++) {
// 图片在可视区冒出的高度
let x = clientHeight + scrollTop - imgs[i].offsetTop
// 图片在可视区内
if (x > 0 && x < clientHeight+imgs[i].height) {
imgs[i].src = imgs[i].getAttribute('data')
}
}
}*/
function lazyLoad () {
imgs.forEach(item=>{
if(item.getBoundingClientRect().top < clientHeight){
imgs[i].src = imgs[i].getAttribute('data')
}
})
}
setTimeout(lazyLoad, 500)
window.addEventListener('scroll', throttle(lazyLoad, 500));
以上代码简单实现了懒加载的效果,但是太简单了,今天准备讲讲它的前世今生,让大家能更好的理解它,那么现在开始吧😊
从浏览器底层渲染机制分析懒加载的意义
前端性能优化一直提到要各种懒加载,那为什么呢?
浏览器渲染页面过程
:构建DOM Tree
-->构建CSSOM
-->结合生成render Tree
-->绘制
构建DOM树中如果遇到img
老版本:阻碍DOM渲染
新版本:不会阻碍 每一个图片请求都会占用一个HTTP(浏览器同时发送的HTTP 6个,因为http1.1中最多可以并发6个tcp),那么就会耽误其他资源的拉取,回来资源后会和RENDER TREE一起渲染,会让页面第一次加载变慢(白屏)
图片延迟懒加载就是:第一次不请求也不渲染图片,等页面加载完,其他资源都渲染好了,再去请求加载屏幕区图片
效果
瀑布流思路
这里简单说一下瀑布流的实现,首先有四列,将每个columns高度获取到,根据高度来进行瀑布流,数据四个一组,img高度最大的放在高度最小的columns中,高度最小的放在高度最大的columns中,这样就能保证四列各自的高度不会相差太大。详情请看代码中createHTML()
懒加载思路
- 真实图片地址存放在自定义属性中,比如
<img src="" real-src="./images/1.jpg" />
- 获取到页面中所有img元素,判断是否在可视区域,在就将
img.src = img.getAttribute("real-src")
- 监听scroll事件,可以配合函数节流,查看
utils中的throttle
注意:最关键就在于如何去判断是否在可视区域
最初基于JS盒模型实现的懒加载方案
基于JS盒模型就是利用offsetTop、scrollTop来判断
所以我们可以写出这样的代码
function lazyFunc() {
console.log('ok!')
if (!itemBoxs) { //第一次没有就获取,不用每次获取,提高性能
itemBoxs = Array.from(document.querySelectorAll('.itemBox'));
}
itemBoxs.forEach(itemBox => {
// 已经处理过则不在处理
let isLoad = itemBox.getAttribute("isLoad");
if (isLoad) return;
/* 加载条件:盒子顶边距离BODY距离 < 浏览器视口高度 + scrollTop */
// let B = utils.offset(itemBox).top + itemBox.offsetHeight,
let B = utils.offset(itemBox).top,
A = winH + document.documentElement.scrollTop;
if (B <= A) {
lazyImgFunc(itemBox);
}
})
}
function lazyImgFunc(itemBox) {
let img = itemBox.querySelector("img"),
trueImg = img.getAttribute("real-src");
img.src = trueImg;
img.onload = function () {
// 图片加载成功
utils.css(img, 'opacity', 1);
}
img.removeAttribute("real-src")
// 记录图片已经处理过了
itemBox.setAttribute("isLoad", true)
}
基于getBoundingClientRect的进阶方案
getBoundingClientRect用于获取某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。
left: 元素左边距浏览器左边距离
right: 元素右边距浏览器左边距离
top: 元素顶边距浏览器顶边距离
bottom: 元素底边距浏览器顶边距离
那么判断的条件就是img盒子.getBoundingClientRect().top <= 浏览器可视区高,这样是不是就好理解多了
function lazyFunc() {
console.log('ok!')
if (!itemBoxs) { //第一次没有就获取,不用每次获取,提高性能
itemBoxs = Array.from(document.querySelectorAll('.itemBox'));
}
itemBoxs.forEach(itemBox => {
// 已经处理过则不在处理
let isLoad = itemBox.getAttribute("isLoad");
if (isLoad) return;
/* getBoundingClientRect
left: 元素左边距浏览器左边距离
right: 元素右边距浏览器左边距离
top: 元素顶边距浏览器顶边距离
bottom: 元素底边距浏览器顶边距离
*/
let {
top,
bottom
} = itemBox.getBoundingClientRect();
if (top <= winH) {
lazyImgFunc(itemBox);
}
})
}
function lazyImgFunc(itemBox) {
let img = itemBox.querySelector("img"),
trueImg = img.getAttribute("real-src");
img.src = trueImg;
img.onload = function () {
// 图片加载成功
utils.css(img, 'opacity', 1);
}
img.removeAttribute("real-src")
// 记录图片已经处理过了
itemBox.setAttribute("isLoad", true)
}
终极方案:IntersectionObserver
IntersectionObserver类似于事件监听,发布订阅模式,我们可以通过一个简单的例子来了解下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载</title>
<link rel="stylesheet" href="css/reset.min.css">
<style>
.box {
width: 300px;
margin: 1300px auto;
}
.box img {
width: 100%;
}
</style>
</head>
<body>
<div class="box" id="box">
<img src="images/1.jpg" alt="">
</div>
<script>
let observer = new IntersectionObserver(changes => {
// changes包含所有监听对象的信息
// target当前监听的对象
// isIntersecting 是否出现在视口中
// boundingClientRect
// ...
console.log(changes);
let item = changes[0];
if (item.isIntersecting) {
// 进入到视口
// ...
observer.unobserve(item.target);
}
});
observer.observe(document.querySelector("#box"));
</script>
</body>
</html>
当我们监听observer.observe(document.querySelector("#box"))
函数就会触发,当元素进入或者离开视口也会触发,可以通过isIntersecting
属性来判断是否进入到视口,不用监听scroll事件,做到元素一可见便触发回调。不需要监听scroll,也不需要函数节流,性能比较好。
let observer = new IntersectionObserver(changes => {
changes.forEach(item => {
let { target, isIntersecting } = item;
if (isIntersecting) {
lazyImgFunc(target)
observer.unobserve(target)
}
})
})
function lazyFunc() {
console.log('ok!')
if (!itemBoxs) { //第一次没有就获取,不用每次获取,提高性能
itemBoxs = Array.from(document.querySelectorAll('.itemBox'));
}
itemBoxs.forEach(itemBox => {
observer.observe(itemBox)
})
}
function lazyImgFunc(itemBox) {
let img = itemBox.querySelector("img"),
trueImg = img.getAttribute("real-src");
img.src = trueImg;
img.onload = function () {
// 图片加载成功
utils.css(img, 'opacity', 1);
}
img.removeAttribute("real-src")
}
未来设想:img.loading=lazy
现在chrome 76已经 支持了,可以通过loading="lazy"
来实现
<img src="./images/1.jpg" alt="" loading="lazy">