一晚带你玩转图片懒加载及其底层原理
课程大纲
- 从浏览器底层渲染机制分析懒加载的意义
- 最初基于JS盒模型实现的懒加载方案
- 基于getBoundingClientRect的进阶方案
- 手撕Lodash源码中的debounce(函数防抖)
- 手撕Lodash源码中的throttle(函数节流)
- 终极方案:IntersectionObserver
- 未来设想:img.loading=lazy
基于JS盒模型的花瓣网瀑布流懒加载
一. 实现思路
比如页面就是一个容器,里面有三列,分别从服务器拿到很多数据,比如从服务器拿到50条数据。50条数据按照一定规则插入到三列当中,首先我会把50条数据里的前3条拿到,第一次插的时候直接往进插就可以了,每个card的图片大小不一样,每个card的高度也就不一样,宽度固定,但高度不一样。现在已经把前3个数据插进去了,3个插完之后,我从50个数据里再拿下一组三个,我首先会看一下这三列当中现在的高度的排列顺序,然后按三列现有的高度按它的内容由高到低进行排序,并且也会把我拿到的3条数据进行由低到高进行排序。把当前拿到的3条数据中最小的最低的插入上一条数据最高的那一列里;把第二小的插入到第二列里,把当前拿到的最高的插入到最小的列。这样就保证三列布局每3个往进插每3个往进插,最后三列的高度相差也不是特别大,这就是瀑布流无规则排列。宽度固定,调整图片高度排列顺序。
(二)实现步骤:
1.实现瀑布流效果
代码思路详细解剖
(1)最早期的模块化思想[没有用vue和react]
(2)写业务逻辑,我们会return个对象来,我们会写一个方法,叫init(),init()是我们当前模块的唯一入口。
(3)一会我们再在页面里要干什么都会调init()方法,在init()里控制先干什么后干什么。
(4)未来我们想实现功能,只需要用命名空间或用模块的名字调它的init()方法。
(5)这就是我们早期的基于闭包、基于惰性函数惰性思想的JS高阶编程技巧实现业务开发的模块化思想。
接下来
(6)第一步从服务器获取数据才能干我们接下来的事了,用async await请求utils的ajax方法请求本地里有一个data.json
(7)有数据后接下来该做数据绑定了,写个方法叫bindHTML(),把data传进去实现数据绑定
(8)数据绑定思路:一共有三列,接下来就把从服务器拿到的50条数据每3个为一组分别插入到3列当中,这么一步步处理就好了
但是在处理之前,从服务器拿到的数据data有一个特点,每一个数据里都包含图片的高和宽,宽和高是按照图片本身来的
实现瀑布流就要有宽高,没有宽高就要服务器处理,一般服务器返回的图片都会有宽和高。服务器返回的数据里图片的宽度是300,
但是我们要把数据插入这个列里,每一列是240,每一列左右还有5px padding,真实的是230.把300的图片放到230的区域里呈现
宽度就要缩小,从300缩到230,那高度也要同比例缩小一些才不会导致图片的变形。
(9)根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放。因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能用一个容器先占位。
(10)元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组。
(11)data是50条数据50条数据要每3个去拿。
(12)js盒子模型的13个属性,clientHeight、clientWidth、clientLeft、clientTop、offsetHeight、offsetWidth、offsetLeft、offsetTop、offsetParent、scrollHeight、scrollWidth、scrollLeft、scrollTop
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<title>珠峰在线Web高级课</title>
<!-- IMPORT CSS -->
<link rel="stylesheet" href="css/reset.min.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<div class="container clearfix">
<div class="column">
<!-- <div class="card">
<a href="#">
<div class="lazyImageBox">
<img src="" alt="" data-image="images/1.jpg">
</div>
<p>泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证</p>
</a>
</div> -->
</div>
<div class="column"></div>
<div class="column"></div>
</div>
<!-- IMPORT JS -->
<script src="js/utils.js"></script>
<script src="js/index.js"></script>
</body>
</html>
/*index.js*/
let imageModule=(function(){
//元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组
let columns= Array.from(document.querySelectorAll('.column'));
//数据绑定
function bindHTML(data){
//根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放
//因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度
//这样才能用一个容器先占位
data=data.map(item=>{
let {
width,
height
} = item;
item.height=height/(width/230);
item.width=230;
return item;
});
//每3个为一组获取数据
for(let i = 0;i<data.length;i+=3){
let group =data.slice(i,i+3);
//实现每一列的降序
columns.sort((a,b)=>{
return b.offsetHeight - a.offsetHeight;
});
//把一组数据的进行升序
group.sort((a,b)=>{
return a.height - b.height;
});
//分别把最小数据插入到最大的列中
group.forEach((item,index)=>{
let{
width,
height,
title,
pic
} = item;
let card= document.createElement('div');
card.className = "card";
card.innerHTML =
`<a href="#">
<div class="lazyImageBox" style="height:${height}px">
<img src="" alt="" data-image="${pic}">
</div>
<p>${title}</p>
</a>`;
columns[index].appendChild(card);
});
}
}
return {
async init(){
let data = await utils.ajax('./data.json');
// console.log(data);获取到50条数据了
bindHTML(data);
}
}
})();
imageModule.init();
瀑布流效果
2.图片显示
let imageModule = (function () {
//元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组
let columns = Array.from(document.querySelectorAll('.column'));
//数据绑定
function bindHTML(data) {
//根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放
//因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度
//这样才能用一个容器先占位
data = data.map(item => {
let {
width,
height
} = item;
item.height = height / (width / 230);
item.width = 230;
return item;
});
//每3个为一组获取数据
for (let i = 0; i < data.length; i += 3) {
let group = data.slice(i, i + 3);
//实现每一列的降序
columns.sort((a, b) => {
return b.offsetHeight - a.offsetHeight;
});
//把一组数据的进行升序
group.sort((a, b) => {
return a.height - b.height;
});
//分别把最小数据插入到最大的列中
group.forEach((item, index) => {
let {
width,
height,
title,
pic
} = item;
let card = document.createElement('div');
card.className = "card";
card.innerHTML = `<a href="#">
<div class="lazyImageBox" style="height:${height}px">
<img src="" alt="" data-image="${pic}">
</div>
<p>${title}</p>
</a>`;
columns[index].appendChild(card);
});
}
}
//实现图片的延迟加载
let lazyImageBoxs;
function lazyFunc() {
!lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;
lazyImageBoxs.forEach(lazyImageBox => {
//已经处理过则不再处理
let isLoad = lazyImageBox.getAttribute('isLoad');
if (isLoad) return;
lazyImg(lazyImageBox);
});
}
function lazyImg(lazyImageBox) {
let img = lazyImageBox.querySelector('img'),
trueImg = img.getAttribute('data-image');
img.src = trueImg;
img.onload = function () {
// 图片加载成功
utils.css(img, 'opacity', 1);
};
img.removeAttribute('data-image');
//记录当前图片都处理过了
lazyImageBox.setAttribute('isLoad', 'true');
}
return {
async init() {
let data = await utils.ajax('./data.json');
// console.log(data);获取到50条数据了
bindHTML(data);
setTimeout(lazyFunc, 500);//window.onload也可以
}
}
})();
imageModule.init();
图片显示
3.图片的延迟加载的详细原因、思路及实现
浏览器渲染页面
- 1.构建DOM树
- 2.构建CSSOM树
- 3.生成RENDER TREE
- 4.布局
- 5.分层
- 6.珊格化
- 7.绘制
- 构建DOM树中如果遇到img
- 老版本:阻碍DOM渲染
- 新版本:不会阻碍 每一个图片请求都会占用一个HTTP(浏览器同时发送的HTTP 6个)
- 拿回来资源后会和RENDER TREE一起渲染
- …
- 开始加载图片,一定会让页面第一次渲染速度变慢(白屏)
- 图片延迟加载:第一次不请求也不渲染图片,等页面加载完,其他资源都渲染好了,再去请求加载图片.
懒加载的思路
css
.card a .lazyImageBox {
/* height: xxx; 如果是需要进行图片延迟加载,在图片不显示的时候,我们要让盒子的高度等于图片的高度,这样才能把盒子撑开(服务器返回给我们的数据中,一定要包含图片的高度和宽度) */
/* background: url("../images/default.gif") no-repeat center center #F4F4F4; */
overflow: hidden;
}
html
<div class="lazyImageBox" style="height:${height}px">
<img src="" alt="" data-image="${pic}">
</div>
分析条件
临界点:如图当盒子刚刚完全显示在浏览器当前窗口中时,盒子顶部距离body的偏移量加上盒子本身的高度恰等于滚动条卷去的高度+浏览器的高度。
那么如果盒子底部距离页面顶部的长度小于滚动条卷去的高度+浏览器的高度,那么说明图片完全显示在页面视口中,就需要做延迟加载。
//实现图片的延迟加载
let lazyImageBoxs;
+ let winH = document.documentElement.clientHeight;
function lazyFunc() {
!lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;
lazyImageBoxs.forEach(lazyImageBox => {
//已经处理过则不再处理
let isLoad = lazyImageBox.getAttribute('isLoad');
if (isLoad) return;
+ //加载条件:盒子底边距离BODY距离(盒子顶部距离body的偏移量加上盒子本身的高度)<浏览器距离BODY的高度(滚动条卷去的高度+浏览器的高度)
+ let B=utils.offset(lazyImageBox).top+lazyImageBox.offsetHeight,
+ A=winH+document.documentElement.scrollTop;
+ if(B<=A){
+ lazyImg(lazyImageBox);
}
});
}
那么如何实现随着滚动页面而实现的延迟加载?
return {
async init() {
let data = await utils.ajax('./data.json');
// console.log(data);获取到50条数据了
bindHTML(data);
setTimeout(lazyFunc, 500);//window.onload也可以
+ window.onscroll = lazyFunc;
}
}
})();
基于getBoundingClientRect
的进阶方案
(一)在浏览器中打开1.html。在控制台输入此方法,DOMRect包含了当前的盒子及盒子的样式,最下面的x和y一般不用,因为它兼容性特别差,width和height在ie678下是不兼容的。bottom、left、right、top都是兼容浏览器的。真实项目中已经完全用这种方案代替JS盒模型了,因为盒子模型太麻烦了,要计算很多值
let imageModule = (function () {
//元素集合是类数组集合不是数组,未来想进行排序操作得要转换成数组。用Array.from把类数组集合转换成数组
let columns = Array.from(document.querySelectorAll('.column'));
//数据绑定
function bindHTML(data) {
//根据服务器返回的图片宽高,动态计算出图片放到230容器中,高度应该怎么缩放
//因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度
//这样才能用一个容器先占位
data = data.map(item => {
let {
width,
height
} = item;
item.height = height / (width / 230);
item.width = 230;
return item;
});
//每3个为一组获取数据
for (let i = 0; i < data.length; i += 3) {
let group = data.slice(i, i + 3);
//实现每一列的降序
columns.sort((a, b) => {
return b.offsetHeight - a.offsetHeight;
});
//把一组数据的进行升序
group.sort((a, b) => {
return a.height - b.height;
});
//分别把最小数据插入到最大的列中
group.forEach((item, index) => {
let {
width,
height,
title,
pic
} = item;
let card = document.createElement('div');
card.className = "card";
card.innerHTML = `<a href="#">
<div class="lazyImageBox" style="height:${height}px">
<img src="" alt="" data-image="${pic}">
</div>
<p>${title}</p>
</a>`;
columns[index].appendChild(card);
});
}
}
//实现图片的延迟加载
let lazyImageBoxs;
let winH = document.documentElement.clientHeight;
function lazyFunc() {
!lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;
lazyImageBoxs.forEach(lazyImageBox => {
//已经处理过则不再处理
let isLoad = lazyImageBox.getAttribute('isLoad');
if (isLoad) return;
— //加载条件:盒子底边距离BODY距离(盒子顶部距离body的偏移量加上盒子本身的高度)<浏览器距离BODY的高度(滚动条卷去的高度+浏览器的高度)
_ // let B=utils.offset(lazyImageBox).top+lazyImageBox.offsetHeight,
— // A=winH+document.documentElement.scrollTop;
— // if(B<=A){
— // lazyImg(lazyImageBox);
— // }
+ let {bottom}=lazyImageBox.getBoundingClientRect();
+ if(bottom<=winH){
+ lazyImg(lazyImageBox);
}
});
}
function lazyImg(lazyImageBox) {
let img = lazyImageBox.querySelector('img'),
trueImg = img.getAttribute('data-image');
img.src = trueImg;
img.onload = function () {
// 图片加载成功
utils.css(img, 'opacity', 1);
};
img.removeAttribute('data-image');
//记录当前图片都处理过了
lazyImageBox.setAttribute('isLoad', 'true');
}
return {
async init() {
let data = await utils.ajax('./data.json');
// console.log(data);获取到50条数据了
bindHTML(data);
setTimeout(lazyFunc, 500);//window.onload也可以
window.onscroll = lazyFunc;
}
}
})();
imageModule.init();
这么做了之后,我们当前的延迟就达到我们的效果了吗?No,还没有达到呢?我们说了在index.js,我们刚开始进来要做延迟加载,滚动的时候也要执行lazyFunc做延迟加载。
做个小测验,在lazyFunc(),打印OK。我们发现在浏览器向下滚动时中有很多个OK打印,说明lazyFunc被频繁触发了好多次,虽然最终没有达到条件和定义延迟加载,但这些东西被触发很多次,说明性能就会有所差距。所以在这个基础上要进行优化。
onscroll触发频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能=>我们需要降低onscrll的时候的触发频率(节流)。
return {
async init() {
let data = await utils.ajax('./data.json');
// console.log(data);获取到50条数据了
bindHTML(data);
setTimeout(lazyFunc, 500);//window.onload也可以
//onscroll触发频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能=>我们需要降低onscrll
//的时候的触发频率(节流)
+ window.onscroll = utils.throttle(lazyFunc,500);
}
}
整个频率降低了很多很多,达到了性能优化的过程。
终极方案:IntersectionObserverIntersectionObserver
上一步用的是getBoundingClientRect
+节流进行了性能优化,看起来很好,这种方案现在不需要做什么防抖节流,即使节流也会触发很多没必要的操作,真实想做的操作就是只要它一出来就让它加载。不出来就不管它了,不是在onscroll随时校验,是真正达到这个条件再去做这个事情。那一定比我们的节流还要做的更好。我们节流也只是把之间的频率降低了而已,降低了也会有很多没必要的操作。IntersectionObserverIntersectionObserver
能把上面讲的东西全部优化了,新出来的,这种方案的兼容性不是特别好,低版本浏览器是不兼容的。polyfill处理不了它,移动端不考虑低版本浏览器,一般都是这种方案。但是这个性能超好,不需要节流处理。
**IntersectionObserverIntersectionObserver
**的简介。
1.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>珠峰在线Web高级课</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);
});
observer.observe(box);
</script>
</body>
</html>
由截图可知,这个方法刚开始会触发一次,当滚动到图片出现在视窗口中会再触发一次。完全离开的时候再触发一次。
离开时移除监听
<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(box);
</script>
index2.js
let imageModule = (function () {
let columns = Array.from(document.querySelectorAll('.column'));
// 数据绑定
function bindHTML(data) {
// 根据服务器返回的图片的宽高,动态计算出图片放在230容器中,高度应该怎么缩放
// 因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能又一个容器先占位
data = data.map(item => {
let {
width,
height
} = item;
item.height = height / (width / 230);
item.width = 230;
return item;
});
// 每三个为一组获取数据
for (let i = 0; i < data.length; i += 3) {
let group = data.slice(i, i + 3);
// 实现每一列的降序
columns.sort((a, b) => {
return b.offsetHeight - a.offsetHeight;
});
// 把一组的数据进行升序
group.sort((a, b) => {
return a.height - b.height;
});
// 分别把最小数据插入到最大的列中
group.forEach((item, index) => {
let {
height,
title,
pic
} = item;
let card = document.createElement('div');
card.className = "card";
card.innerHTML = `<a href="#">
<div class="lazyImageBox" style="height:${height}px">
<img src="" alt="" data-image="${pic}">
</div>
<p>${title}</p>
</a>`;
columns[index].appendChild(card);
});
}
}
// 实现图片的延迟加载
// IntersectionObserver 监听DOM对象,当DOM元素出现和离开视口的时候触发回调函数
+ let lazyImageBoxs,
+ observer = new IntersectionObserver(changes => {
+ changes.forEach(item => {
+ console.log(changes)//刚开始有50个
+ let {
+ isIntersecting,
+ target
+ } = item;
+ if (isIntersecting) {//出现在视口中
+ lazyImg(target);
+ observer.unobserve(target);//处理过的移除监听
+ }
+ });
+ });
function lazyFunc() {
!lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;
lazyImageBoxs.forEach(lazyImageBox => {
observer.observe(lazyImageBox);
});
}
function lazyImg(lazyImageBox) {
let img = lazyImageBox.querySelector('img'),
trueImg = img.getAttribute('data-image');
img.src = trueImg;
img.onload = function () {
// 图片加载成功
utils.css(img, 'opacity', 1);
};
img.removeAttribute('data-image');
}
return {
async init() {
let data = await utils.ajax('./data.json');
bindHTML(data);
setTimeout(lazyFunc, 500);
_
_
}
}
})();
imageModule.init();
加载的效果几乎看不到下面没加载图片的空白区域,只有快速滚动才能看到效果。
这个方案还可以实现哪些功能?
加载到底部加载更多数据,在移动端如果不需要考虑太多低版本操作系统,做延迟加载时基本都用这种方案。思路如下:
未来设想:img.loading=lazy
未来的设想,啥也不用管,只要设置lazy,浏览器就会帮我们做延迟加载。 这种方案目前只兼容 Chrome 76,并且窗口高度 网速 滚动 窗口大小改变。
img.loading=lazy ,下一步要做的事情:我们自己在不兼容的情况下,写一个插件,兼容它(其实就是自己去实现一套处理方法).
index.html
<script src="js/utils.js"></script>
<script src="js/index3.js"></script>
index3.js
let imageModule = (function () {
let columns = Array.from(document.querySelectorAll('.column'));
// 数据绑定
function bindHTML(data) {
// 根据服务器返回的图片的宽高,动态计算出图片放在230容器中,高度应该怎么缩放
// 因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能又一个容器先占位
data = data.map(item => {
let {
width,
height
} = item;
item.height = height / (width / 230);
item.width = 230;
return item;
});
// 每三个为一组获取数据
for (let i = 0; i < data.length; i += 3) {
let group = data.slice(i, i + 3);
// 实现每一列的降序
columns.sort((a, b) => {
return b.offsetHeight - a.offsetHeight;
});
// 把一组的数据进行升序
group.sort((a, b) => {
return a.height - b.height;
});
// 分别把最小数据插入到最大的列中
group.forEach((item, index) => {
let {
height,
title,
pic
} = item;
let card = document.createElement('div');
card.className = "card";
// Chrome 76
// 窗口高度 网速 滚动 窗口大小改变 ...
card.innerHTML = `<a href="#">
<div class="lazyImageBox" style="height:${height}px">
<img src="${pic}" alt="" loading="lazy">
</div>
<p>${title}</p>
</a>`;
columns[index].appendChild(card);
});
}
}
return {
async init() {
let data = await utils.ajax('./data.json');
bindHTML(data);
}
}
})();
imageModule.init();
/* // 下一步要做的事情:我们自己在不兼容的情况下,写一个插件,兼容它(其实就是自己去实现一套处理方法)
if ('loading' in (new Image)) {
console.log('ok');
} */
// typeof IntersectionObserver==="undefined"
// ...