一晚带你玩转图片懒加载及其底层原理
课程大纲
- 从浏览器底层渲染机制分析懒加载的意义
- 最初基于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"
// ...