持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天
💡 github完整代码:github.com/babachao/wa…
💡 体验Demo: codesandbox.io/s/divine-cl… (codesandbox加载可能有些慢)
💡 Tips: 建议下载一个Steam++使用,逛Github无阻碍~
💡 写的不对的地方麻烦大伙纠正~,谢谢
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>瀑布流</title>
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<div class="app clearfix"></div>
<script src="./js/utils.js"></script>
<script src="./js/index.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.27.2/axios.min.js"></script>
<script>
// 初始化
const waterfallNew = new Waterfall(3);
waterfallNew.init()
/**
* @description: 发起请求获取数据
*/
const getData = function (type) {
axios.get("./data.json").then((res) => {
waterfallNew.creatDefaultDiv(res.data);
// 调用延迟加载, 数据绑定完后延迟500ms
if (type === 'init') {
setTimeout(() => { waterfallNew.lazyImages() }, 500);
}
});
};
// 到达条件时,触发加载更多数据
const loadMore = function () {
// 滚动到底部-》 屏幕高度 + 卷去的高度 + 500 >= 页面的真实高度
const bodyTopHeight = getBodyTopHeight(500);
const bodyHeight = document.documentElement.offsetHeight;
if (bodyTopHeight >= bodyHeight) {
getData();
}
waterfallNew.lazyImages();
}
// 监听滚动事件
window.onscroll = throttle(loadMore, 300);
getData('init');
</script>
</body>
</html>
JS
class Waterfall {
constructor(column) {
this.column = column;
this.testnum = 0;
this.init = function () {
const app = document.querySelector('.app');
let htmlStr = '';
for (let i = 0; i < column; i++) {
htmlStr += `<div id="${i}" class="column" style="width: ${Math.floor(100 / column)}%;"></div>`;
}
app.innerHTML = htmlStr;
},
/**
* @description: 获取瀑布流的列。默认3列
*/
this.getColumns = function () {
return [...document.querySelectorAll(".column")];
},
/**
* @description: 创建默认的div
* @param { object } data 当前请求的数据
*/
this.creatDefaultDiv = function (data) {
const columns = this.getColumns();
for (let i = 0; i < data.length; i += column) {
// 3个为一组,组成新的数组。
// 1->data.slice(0,3) 2->data.slice(4,7) 依次类推
const combination = data.slice(i, i + column);
// 并且将【column】默认为3个,为一组的数据从高到低排序
combination.sort((a, b) => b.height - a.height);
// 根据dom的offsetHeight来排序,将优先插入矮的【column】
columns
.sort((a, b) => a.offsetHeight - b.offsetHeight)
.forEach((columnDom, index) => {
const dataItem = combination[index];
if (!dataItem) return false;
// columnDom.innerHTML = this.creatInnerHTML(dataItem, columnDom);
columnDom.appendChild(this.createAppendDom(dataItem));
});
}
},
/**
* @description: 基于innerHTML创建imgBox和img标签
* @param {*} dataItem 单个数据item
* @param {*} domItem 单个数据item
* @return {string} 返回组装好的html
*/
this.creatInnerHTML = function (dataItem, columnDom) {
const { url, height, id, text } = dataItem;
const htmlStr = columnDom.innerHTML;
const newHtml = `
<div id="${id}" class="column-item" style="height:${height / 2}px;" isLoad="false" >
<img src="" data-src="${url}" />
<p>${text}</p>
<span>${id}</span>
</div>
`;
return htmlStr + newHtml;
},
/**
* @description: 创建div->img节点,appendChild到每一列中
* @param { object } dataItem 请求下来的单条数据
*/
this.createAppendDom = function (dataItem) {
const { url, height, id, text } = dataItem;
const createDiv = document.createElement("div");
const span = document.createElement("span");
const p = document.createElement("p");
const imgDom = new Image();
p.innerHTML = text;
span.innerHTML = id;
imgDom.setAttribute("data-src", url);
createDiv.classList.add("column-item");
createDiv.setAttribute("id", id);
createDiv.setAttribute("isLoad", "false");
createDiv.style.height = height / 2 + "px"; // 因为我用的图片高度太高了,所以除2了
createDiv.appendChild(imgDom);
createDiv.appendChild(p);
createDiv.appendChild(span);
return createDiv;
},
/**
* @description: 实现懒加载功能
*/
this.lazyImages = function () {
console.log('次数', this.testnum);
console.time('循环时间:')
const bodyTopHeight = getBodyTopHeight(); // 获取可视高度 + 滚动高度(此方法在utils.js中)
const colums = this.getColumns();
for (let j = 0; j < colums.length; j++) {
const columnItems = [...colums[j].querySelectorAll(".column-item[isLoad='false']")]; // 获取还没有加载过的box
for (let i = 0; i < columnItems.length; i++) {
const imgBox = columnItems[i];
const imgBoxHeight = imgBox.offsetHeight + imgBox.offsetTop; // 当前imgBox的高 + 距离顶部的高
this.testnum += 1;
// // imgBox的高 + 距离顶部的高 <= body高 + 滚动的距离时,进入判断并且将图片加载出来
if (imgBoxHeight <= bodyTopHeight) {
imgBox.setAttribute('isLoad', 'true');
this.handleImg(imgBox.children[0]);
} else {
break;
}
}
}
console.timeEnd('循环时间:')
},
/**
* @description: 实现懒加载功能
* @param { object } imgItem 当前img的Dom节点
*/
this.handleImg = function (imgItem) {
const url = imgItem.getAttribute("data-src"); // 获取到图片的链接
imgItem.setAttribute("src", url); // 设置给img的src
// 加载成功后设置style
imgItem.onload = () => {
imgItem.style.opacity = 1;
imgItem.style.transition = "0.6s";
};
imgItem.onerror = (e) => {
// xxx 若加载失败,可在这展示默认图片
};
}
}
}
CSS
html,
body {
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.clearfix::after {
content: "";
display: block;
visibility: hidden;
clear: both;
}
.app {
padding: 0.4%;
}
.app>.column {
/* width: 32.5%; */
float: left;
background-color: #fff;
}
.app .column-item {
position: relative;
width: 100%;
overflow: hidden;
margin-bottom: 10px;
background: url("..//img/load.gif") no-repeat center center;
background-size: 70%;
background-color: #e8e8e8;
box-shadow: 0 0 6px 6px #ccc;
}
.app .column-item p {
display: none;
color: #fff;
text-align: center;
}
.app .column-item span {
position: absolute;
top: 40%;
left: 50%;
color: #fff;
font-size: 20px;
display: none;
}
.app>div:nth-last-child(2n) {
margin: 0 0.5%;
}
.app img {
opacity: 0;
width: 100%;
height: 100%;
}
JS-原理分析
1、实现init()方法-默认3列
💡 Tips:3列的话,width宽度为33.3333%,对应列的width需要自己修改一下。
💡循环column遍,生成对应列,放入到div容器中
// column 默认为3,通过传参拿到
const init = fucntion (column) {
const app = document.querySelector('.app');
let htmlStr = '';
for (let i = 0; i < column; i++) {
htmlStr += `<div class="column"></div>`;
}
app.innerHTML = htmlStr;
}
2、实现creatDefaultDiv()方法,创建默认的imgBox。
💡 Tips:将数据遍历分为3组并且排序,创建装有img的div 实现getColumns方法,获取所有列 💡 知识点: 1、offsetHeight 返回该元素的像素高度 2、querySelectorAll 返回与指定的选择器组匹配的文档中的元素列表 3、setAttribute 设置指定元素上的某个属性值(有则更新,无则添加)
const getColumns = function () {
return [...document.querySelectorAll(".column")];
}
const creatDefaultDiv = function (data) {
const columns = getColumns();
for (let i = 0; i < data.length; i += column) {
// 3个为一组,组成新的数组。
// 1->data.slice(0,3) 2->data.slice(4,7) 依次类推
const combination = data.slice(i, i + column);
// 并且将【column】默认为3个,为一组的数据从高到低排序
combination.sort((a, b) => b.height - a.height);
// 根据dom的offsetHeight来排序,将优先插入矮的【column】
columns
.sort((a, b) => a.offsetHeight - b.offsetHeight)
.forEach((columnDom, index) => {
const dataItem = combination[index];
if (!dataItem) return false;
columnDom.appendChild(createAppendDom(dataItem));
});
}
}
💡 imgBox设置一张默认加载的背景图
/**
* @description: 创建默认的div
* @param { object } data 当前请求的数据
*/
const createAppendDom = function (dataItem) {
const { url, height, id, text } = dataItem;
const createDiv = document.createElement("div");
const span = document.createElement("span");
const p = document.createElement("p");
const imgDom = new Image();
p.innerHTML = text;
span.innerHTML = id;
imgDom.setAttribute("data-src", url);
createDiv.classList.add("column-item");
createDiv.setAttribute("id", id);
createDiv.setAttribute("isLoad", "false");
createDiv.style.height = height + "px";
createDiv.appendChild(imgDom);
createDiv.appendChild(p);
createDiv.appendChild(span);
return createDiv;
}
3、实现lazyImages()懒加载功能
💡 Tips:(img的opacity默认是0;)
1、创建img时,将url地址绑在data-src上 2、需要展示img时,通过setAttribute()设置url,加载图片 3、给img.style.opacity = 1和 img.style.transtion = '0.6s',添加一个渐现的动画 💡 知识点: 1、使用querySelectorAll 属性选择器查找对应的【.column-item】DOM 2、offsetTop 返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。
/**
* @description: 实现懒加载功能
*/
const lazyImages = function () {
const bodyTopHeight = getBodyTopHeight(); // 获取页面可视高度 + 滚动高度(此方法在utils.js中)
const colums = getColumns(); // 获取所有列
// 先循环列,再循环imgBox
for (let j = 0; j < colums.length; j++) {
const columnItems = [...colums[j].querySelectorAll(".column-item[isLoad='false']")]; // 通过属性获取还没有加载过的imgBox
for (let i = 0; i < columnItems.length; i++) {
const imgBox = columnItems[i];
const imgBoxHeight = imgBox.offsetHeight + imgBox.offsetTop; // 当前imgBox的高 + 距离顶部的高
// imgBox的高 + 距离顶部的高 <= body高 + 滚动的距离时,进入判断并且将图片加载出来
if (imgBoxHeight <= bodyTopHeight) {
imgBox.setAttribute('isLoad', 'true');
handleImg(imgBox.children[0]); // 处理img的展示
} else {
break;
}
}
}
}
/**
* @description: 实现懒加载功能
* @param { object } imgItem 当前img的Dom节点
*/
const handleImg = function (imgItem) {
const url = imgItem.getAttribute("data-src"); // 获取到图片的链接
imgItem.setAttribute("src", url); // 设置给img的src
// 加载成功后设置style
imgItem.onload = () => {
imgItem.style.opacity = 1;
imgItem.style.transition = "0.6s";
};
imgItem.onerror = (e) => {
// xxx 若加载失败,可在这展示默认图片
};
}
utils.js
/**
* @description: 防抖函数的实现
* @param { Function } fn 需要调用的方法
* @param { number } wait 等待的时间
* @param { boolean } immediate 是否需要在开始边界触发
*/
const debounce = function (fn, wait, immediate = false) {
let result = null; // 储存返回的result
let timeOut = null; // 记录定时器
return function (...args) {
const context = this; // 获取this指向
const now = immediate && !timeOut; //如果需要在开始边界执行,则需要immediate为true并且没有timeOut
clearTimeout(timeOut); // 关键:每次进来时,需要清除上一个定时器,防抖目的是等待时间内只执行一次
timeOut = setTimeout(() => {
if (!immediate) result = fn.call(context, ...args); // 结束边界触发
timeOut = null;
}, wait);
// 开始边界触发
if (now) result = fn.call(context, ...args);
return result;
};
};
/**
* @description: 节流
* @param { Function } fn 需要调用的方法
* @param { number } wait 等待的时间
*/
const throttle = function (fn, wait){
var timer = null;
return function(...args){
var context = this;
clearTimeout(timer);
timer = setTimeout(function(){
fn.call(context, args);
}, wait);
};
};
/**
* @description: 计算: 屏幕高度 + 卷去的高度
* @param { number } addHeight 需要增加的高度
* @return { number } 返回计算出来的结果
*/
const getBodyTopHeight = function (addHeight = 0) {
return document.documentElement.clientHeight + document.documentElement.scrollTop + addHeight;
}
总结 💡
1、该瀑布流实现方案是在接口已经返回了img的高度情况下,不需要一张一张等待img的图片加载完再计算高度,而且这种方案用户体验更好,还可以实现懒加载功能
2、还有就是图片的宽、高可以在B端中上传时拿到,将宽高拼接到img链接后面,或者当参数传给后端,具体方案可以与后端商定好