文章不短(我写的很详细) 耐心看完 给你一个极好性能的瀑布流及图片懒加载
瀑布流效果
何为瀑布流?
瀑布流,又称瀑布流式。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格。
即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。
那么规则是什么呢?
下面通过图解来分析一下瀑布流的算法。
图解瀑布流算法
当第一排排满足够多的等宽图片时(如下图情况),自然而然的考虑到之后放置的图片会往下面排放。
那么第六张图片,放置在什么位置呢?是下图的位置么?
答案是:不是,那我们应该怎样排列呢?
为了减小每列的差距我们应该将后面六张图片中最高的那一个放到当前这六列中最矮的那一列,正数第二高的图片放到倒数第二矮的那一列......以此类推
- 好了,实现的思想大家已经看了,接下来咱们用代码实现一个三列的瀑布流
实现代码
- 先看HTML、CSS
- HTML
<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>
</body>
- CSS
html,
body {
background: #D6D7DB;
}
.container {
box-sizing: border-box;
margin: 20px auto;
width: 760px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.container .column {
box-sizing: border-box;
width: 240px;
}
.card {
margin-bottom: 10px;
padding: 5px;
background: #FFF;
box-shadow: 3px 3px 10px 0 #222;
}
.card a {
display: block;
}
.card a .lazyImageBox {
overflow: hidden;
}
.card a .lazyImageBox img {
width: 100%;
}
.card a p {
margin-top: 5px;
color: #000;
font-size: 12px;
line-height: 20px;
}
- 创造了一个宽度为760px的容器 使用css3弹性盒使内容两端对齐然后将内容分为3列,每列240 像素
- 获取数据
- 模拟数据(创建data.json)
<data.json>
[
{
"id": 1,
"pic": "images/9.jpg",
"width": 300,
"height": 433,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 2,
"pic": "images/5.jpg",
"width": 300,
"height": 200,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 3,
"pic": "images/3.jpg",
"width": 300,
"height": 170,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 4,
"pic": "images/2.jpg",
"width": 300,
"height": 300,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 5,
"pic": "images/3.jpg",
"width": 300,
"height": 170,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 6,
"pic": "images/10.jpg",
"width": 300,
"height": 257,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 7,
"pic": "images/6.jpg",
"width": 300,
"height": 400,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
},
{
"id": 8,
"pic": "images/5.jpg",
"width": 300,
"height": 200,
"title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
"link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
}
]
- 创建utils.js
- 简单封装ajax
<utils.js>
class utils {
constructor() {
console.log('工具类');
}
/**
* 封装Ajax
* @param {*} url
* @returns {Promise}
*/
ajax (url) {
return new Promise(resolve => {
let xhr = new XMLHttpRequest;
xhr.open('get', url);
xhr.onreadystatechange = function () {
(xhr.readyState === 4 && xhr.status === 200) && resolve(JSON.parse(xhr.responseText));
};
xhr.send();
});
}
}
- 使用ajax获取数据并渲染页面 注意看注释
let data = await utils.ajax('./data.json');
bindHTML(data);
// 获取到所有class为column的元素并将类数组转化为数组实例
let columns = Array.from(document.querySelectorAll('.column'));
/**
* 数据绑定
* @param {Array} data
*/
function bindHTML (data) {
// 根据服务器返回的图片的宽高,动态计算出图片放在230容器中,高度应该怎么缩放
// 因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能又一个容器先占位
// 根据容器宽度(230)等比缩放数据中的图片
data.forEach(item => {
let { width, height } = item;
item.height = height / (width / 230);
item.width = 230;
})
// 每三个为一组获取数据
for (let i = 0; i < data.length; i += 3) {
// 将数组分为3列一组
let group = data.slice(i, i + 3);
// 获取到当前每一列的高度并进行升序处理
columns.sort((a, b) => b.offsetHeight - a.offsetHeight);
// 把每一组的数据按照图片高度进行降序处理
group.sort((a, b) => a.height - b.height);
// 把告诉最小的图片插入到高度最大的列中 (看不懂继续看图解)
group.forEach((item, index) => {
let { height, title, pic } = item;
// 创建存放图片的容器
let card = document.createElement('div');
card.className = "card";
// 使用字符模板(es6)为容器添加内容
card.innerHTML = `<a href="#">
<div class="lazyImageBox" style="height:${height}px">
<img src="${pic}" alt="">
</div>
<p>${title}</p>
</a>`;
// 向每一列中添加数据
columns[index].appendChild(card);
});
}
}
- 至此我们实现了瀑布流---接下来我们将实现图片的懒加载
/*
* 为啥要做图片的延迟加载
* 浏览器渲染页面
* 1.构建DOM树
* 2.构建CSSOM树
* 3.生成RENDER TREE (渲染树)
* 4.布局
* 5.分层
* 6.珊格化
* 7.绘制
* 构建DOM树中如果遇到img
* 老版本浏览器:阻碍DOM渲染
* 新版本浏览器:不会阻碍 每一个图片请求都会占用一个HTTP(浏览器同时发送的HTTP 6个)
* 拿回来资源后会和RENDER TREE一起渲染
* .....
* 开始加载图片,一定会让页面第一次渲染速度变慢(白屏)
*
* 图片延迟加载:第一次不请求也不渲染图片,等页面加载完,其他资源都渲染好了,再去请求加载图片
*/
懒加载的优点
- 增强用户体验
- 优化代码
- 减少http的请求
- 减少服务器端压力
- 服务器的按需加载
懒加载的原理
先将img标签中的src链接设为同一张图片(空白图片),将其真正的图片地址存储再img标签的自定义属性中(比如data-src)。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。
这样做能防止页面一次性向服务器响应大量请求导致服务器响应慢,页面卡顿或崩溃等问题。
- 接下来我将使用 getBoundingClientRect方法实现图片大的懒加载
getBoundingClientRect 方法兼容性较好 但不是唯一的解决方案 在文章最后我会简单介绍几种其他的几种方案
- 首先我们来了解下 getBoundingClientRect
理解:getBoundingClientRect用于获取某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。
返回值类型
- top:元素上边到视窗顶部的距离;
- right:元素右边到视窗顶部的距离;
- bottom:元素下边到视窗顶部的距离;
- left:元素左边到视窗顶部的距离;
-
图解
接下来我们使用代码实现图片的懒加载 注意看注释
// 创建一个对象存放需要加载的图片列表
let lazyImageBoxs;
// 获取浏览器可视区域高度
let winH = document.documentElement.clientHeight;
function lazyFunc () {
!lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;
lazyImageBoxs.forEach(lazyImageBox => {
// 已经处理过则不在处理 详情请看 lazyImg 函数
let isLoad = lazyImageBox.getAttribute('isLoad');
if (isLoad) return;
// 若图片底部距离浏览器顶部的距离小于浏览器的可视区域高度 那么说明此图片已完全显示出来
let { bottom } = lazyImageBox.getBoundingClientRect();
bottom <= winH && lazyImg(lazyImageBox);
});
}
/**
* 懒加载图片
* @param {dom} lazyImageBox
*/
function lazyImg (lazyImageBox) {
// img:获取到当前元素中的图片,trueImg:需要渲染的图片路径
let img = lazyImageBox.querySelector('img'),
trueImg = img.getAttribute('data-image');
// 加载图片
img.src = trueImg;
// 移除自定义属性
img.removeAttribute('data-image');
// 添加isLoad属性:表示当前图片已经处理过了
lazyImageBox.setAttribute('isLoad', 'true');
}
// 当页面渲染完成或滚动时都执行图片懒加载函数
window.onload = lazyFunc;
window.onscroll = lazyFunc;
- 至此我们就实现了图片的懒加载
问题:注意看以下两行代码
window.onload = lazyFunc;
window.onscroll = lazyFunc;
这样的弊端是什么
onscroll触发的频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能
- 演示
我们在以下函数中 添加console.log('OK'); 看页面滚动的时候会触发多少次
function lazyImg (lazyImageBox) {
console.log('OK');
//......
}
接下来我们看控制台的输出
这是我滑动滚轮5次函数执行的次数,看到这个数字你是否为页面的性能感到焦虑,那么应该如何处理这种问题呢?
这就用到了 函数节流
接下来我们在utils 工具类中写一个节流方法
class utils {
constructor() {
console.log('工具类');
}
/*
* throttle:实现函数的节流(目的是频繁触发中缩减频率)
* @params
* func:需要执行的函数
* wait:自己设定的间隔时间(频率)
* @return
* 可被调用执行的函数
*/
throttle (func, wait = 500) {
let timer = null,
previous = 0; //记录上一次操作时间
return function anonymous (...params) {
let now = new Date(), //当前操作的时间
remaining = wait - (now - previous);
if (remaining <= 0) {
// 两次间隔时间超过频率:把方法执行即可
clearTimeout(timer);
timer = null;
previous = now;
func.call(this, ...params);
} else if (!timer) {
// 两次间隔时间没有超过频率,说明还没有达到触发标准呢,设置定时器等待即可(还差多久等多久)
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
previous = new Date();
func.call(this, ...params);
}, remaining);
}
};
}
/**
* 封装Ajax
* @param {*} url
* @returns {Promise}
*/
ajax (url) {
return new Promise(resolve => {
let xhr = new XMLHttpRequest;
xhr.open('get', url);
xhr.onreadystatechange = function () {
(xhr.readyState === 4 && xhr.status === 200) && resolve(JSON.parse(xhr.responseText));
};
xhr.send();
});
}
}
- 最后来so easy啦
window.onload = lazyFunc;
window.onscroll = utils.throttle(lazyFunc, 500);
-
让我们看下节流后的效果
我们可以看到同样是滚动5次 函数的执行次数由149 次变为11次
其他懒加载方式:
- new IntersectionObserver
- 直接为img 添加loading="lazy"属性:目前只有Chrome64 版本已上才兼容 感觉这将是未来的趋势
具体实现方法我就不一一写出来了,大家有兴趣的可以了解下,这两种方法比现在用的这种方法要好美中不足的是兼容性还不行