深入探秘jquery瀑布流的实现
瀑布流也应该算是流行几年了吧。首先是由Pinterest掀起的浪潮,然后国内设计如雨后春笋般,冒出很多瀑布流的例子,比如,蘑菇街,Mark之(不过最近涉黄,好像被喝茶了),还有淘宝的“哇哦”.这些都是很棒的例子,今天我想重新谈起瀑布流,一是想满足我自己的愿望,写一个详细的介绍(不敢自名为教程),二是,给大家一份参考,因为search很多,但是他们给的是一个插件,然后教你怎样配置,当然,也有很多其他的大神也细心的做了很多教程,比如imooc上面Amy姐姐发布的瀑布流教程,也是很棒的。而我的目的就是,尽量把一些常见的demo给大家讲解一遍,以及,融合以前学过的设计模式,相当于一个简单的demo.
绝对式布局
不多说,先看一个demo
js
varcal=(function(){
"usestrict";
var$=function(){
returndocument.querySelectorAll.apply(document,arguments);
}
vararrHeight=[];//得到分列的高度
varcolumns=function(){//计算页面最多可以放多少列
varbodyW=document.body.clientWidth,
pinW=$(".pin")[0].offsetWidth;
returnMath.floor(bodyW/pinW);
}
vargetIndex=function(arr){//获得最小高度的index
varminHeight=Math.min.apply(null,arr);//获得最小高度
for(variinarr){
if(arr[i]===minHeight){
returni;
}
}
}
//根据列数确定第一排img的高度并放入数组当中。
varsetWidth=function(){//通过列数设置宽度
varcol=columns(),//获得最后一列
main=$('#main')[0];//获得罩层
main.style.width=col*$('.pin')[0].offsetWidth+"px";
}
varoverLoad=function(ele){
varindex=getIndex(arrHeight),
minHeight=Math.min.apply(null,arrHeight),//获取最小高度
pins=$('.pin'),
style=ele.style;
style.position="absolute";
style.top=minHeight+"px";//设置当前元素的高度
style.left=pins[index].offsetLeft+"px";
arrHeight[index]+=ele.offsetHeight;
}
varinit=function(){
varpins=$(".pin"),
col=columns();
setWidth();//设置包裹容器的宽度
for(vari=0,pin;pin=pins[i];i++){
if(i<col){//存储第一排的高度
arrHeight.push(pin.offsetHeight);
}else{
overLoad(pin);//将元素的位置重排
}
}
}
window.onload=init;
//...执行自动更新操作。
//添加当,滚动到一定位置的时候,添加html节点.
//得到最低高度和序号,获得临界位置
//模仿加载数据
vardataInt=[{
'src':'1.jpg'
},{
'src':'2.jpg'
},{
'src':'3.jpg'
},{
'src':'4.jpg'
},{
'src':'1.jpg'
},{
'src':'2.jpg'
},{
'src':'3.jpg'
},{
'src':'4.jpg'
}];
functionisLoad(){//是否可以进行加载
varscrollTop=document.documentElement.scrollTop||document.body.scrollTop,
wholeHeight=document.documentElement.clientHeight||document.body.clientHeight,
point=scrollTop+wholeHeight;//页面底部距离header的距离
vararr=$('.pin');
varlastHei=arr[arr.length-1].getBoundingClientRect().top;
return(lastHei<point)?true:false;
}
vardealScroll=(function(){
varmain=$('#main')[0],
flag=true;
returnfunction(){
console.log("trigger");
if(isLoad()&&flag){
for(vari=0,data;data=dataInt[i++];){
vardiv=document.createElement('div');
div.innerHTML=temp(data.src);
div.className="pin";
main.appendChild(div);
overLoad(div);//和上面的overload有耦合性质
}
flag=false;
setTimeout(function(){//控制滑行手速,时间越长对速度的滑动时间影响越大。
flag=true;
},1000);
}
}
})();
window.addEventListener('scroll',function(){
dealScroll();
},false);
functiontemp(src){
return`
<divclass="box">
<imgsrc="http://cued.xunlei.com/demos/publ/img/P_00${src}"/>
</div>
`;
}
})();
CSS
$font:10px/1.5sans-serif,"MicrosoftYaHei","Arial";
html{
height:100%;
width:100%;
font:$font;
}
#main{
position:relative;
&::after{
display:block;
content:'';
clear:both;
}
}
.pin{
padding:15px0015px;
float:left;
}
.box{
padding:10px;
border:1pxsolid#ccc;
box-shadow:006px#ccc;
border-radius:5px;
}
.boximg{
width:162px;
height:auto;
}
HTML
<divid="main"> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_001.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_002.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_003.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_004.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_005.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_006.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_007.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_008.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_009.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_010.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_011.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_012.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_013.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_014.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_015.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_016.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_017.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_018.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_019.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_020.jpg"/> </div> </div> <divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_021.jpg"/> </div> </div> </div> <scripttype="text/javascript"src="./dist/index.entry.js"></script>
这个应该算是最常用的一个布局,但是他耗费的性能较高。不过,通过一些重构设计,这些都是可以避免的,我们详细了解一下。
关于html和css,大家可以查看一下控制台,或者直接copy一份都是可以的。
<divclass="pin"> <divclass="box"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_001.jpg"/> </div> </div>
这是main容器框内部的一个pin框,也是最基本的布局单元。(这里,要感谢迅雷UED提供的图片)而我们的页面其实也是由pin单元拼接起来,在内部设置padding,形成间隔的效果。
这里就不详细展开了。
接下来我们看一下js原理。
JS实现原理
其实瀑布式主要的难点就在于,如果将图片整齐的排列在对应的列下,以及什么时候开始刷新加载图片。
而图片整齐的排列的主要逻辑和算法即,先获取容器内可以放多少列,然后,通过计算,存放第一列的高度,再遍历剩下(除第一列的元素)的高度,分别放入,高度最小的那一列。逐个添加,最后结束遍历。
开始刷新的设置就很简单了,瀑布流刷新只和一个事件有关,即,window.onscroll.主要的算法即,当页面滑动到最低高度的时候开始加载节点并且添加,当然,节点添加的个数是不固定的。
先上代码吧,我这里分3部分讲解,一个是图片的排列,一个是设置加载的位置.另外再补充一个响应式加载。
图片排列
var$=function(){
returndocument.querySelectorAll.apply(document,arguments);
}
vararrHeight=[];//得到分列的高度
varcolumns=function(){//计算页面最多可以放多少列
varcontainerW=$("#main")[0].clientWidth,
pinW=$(".pin")[0].offsetWidth;
returnMath.floor(containerW/pinW);
}
vargetIndex=function(arr){//获得最小高度的index
varminHeight=Math.min.apply(null,arr);//获得最小高度
for(variinarr){
if(arr[i]===minHeight){
returni;
}
}
}
//根据列数确定第一排img的高度并放入数组当中。
varsetCenter=(function(){//通过列数设置宽度
varmain=$('#main')[0];//获得罩层
vargetPadding=function(){//设置padding
varcol=columns();//获得最后一列
varpadding=main.clientWidth-col*$('.pin')[0].offsetWidth;
returnpadding/2;
}
vargetComputedStyle=function(ele){//兼容IE的支持情况
if(window.getComputedStyle){
returnwindow.getComputedStyle(ele);
}else{
returnele.currentStyle;
}
}
vargetPinPad=function(){//获得pin的padding值
varpin=$(".pin")[0];
returnparseInt(getComputedStyle(pin).paddingLeft);
}
returnfunction(){//设置宽度
main.style.padding=`0${getPadding()}px0${getPadding()-getPinPad()}px`;
}
})();
varoverLoad=function(ele){
varindex=getIndex(arrHeight),
minHeight=Math.min.apply(null,arrHeight),//获取最小高度
pins=$('.pin'),
style=ele.style;
style.position="absolute";
style.top=minHeight+"px";//设置当前元素的高度
style.left=pins[index].offsetLeft+"px";
arrHeight[index]+=ele.offsetHeight;
}
//初始化时执行函数
varinit=function(){
varpins=$(".pin"),
col=columns();
setCenter();//设置包裹容器的宽度
for(vari=0,pin;pin=pins[i];i++){
if(i<col){//存储第一排的高度
arrHeight.push(pin.offsetHeight);
}else{
overLoad(pin);//将元素的位置重排
}
}
}
里面一共有7个函数(大函数),一个变量。说一下思路吧。首先,页面onload之后执行的函数是init.要知道,一个js程序一定有他的入口,关键看你怎么找了。然后我们深入init函数体,观察。init里面执行的业务逻辑就是存储第一排元素的高度,然后重排剩下的元素。通过columns函数来获得当前窗口最多可以放下多少列,然后设置容器的居中(通过padding设置即可).接下来,遍历pin的单元框,将第一排的元素高度存放在arrHeight数组里,将剩下的元素进行重排。其他的函数觉得没什么讲解的必要了。我们着重来看一下overLoad函数.
overLoad
varoverLoad=function(ele){
varindex=getIndex(arrHeight),
minHeight=Math.min.apply(null,arrHeight),//获取最小高度
pins=$('.pin'),
style=ele.style;
style.position="absolute";
style.top=minHeight+"px";//设置当前元素的高度
style.left=pins[index].offsetLeft+"px";
arrHeight[index]+=ele.offsetHeight;
}
在overLoad里面有getIndex函数,获取最小高度的index,然后就可以设置传入进来的ele元素的位置(绝对定位),top为数组中最小高度的px值,left就为第一排制定的Index元素的左边距位置。最后更新一下高度,ok!!!that'senough.
设置加载位置
vardataInt=[{
'src':'1.jpg'
},{
'src':'2.jpg'
},{
'src':'3.jpg'
},{
'src':'4.jpg'
},{
'src':'1.jpg'
},{
'src':'2.jpg'
},{
'src':'3.jpg'
},{
'src':'4.jpg'
}];
functionisLoad(){//是否可以进行加载
varscrollTop=document.documentElement.scrollTop||document.body.scrollTop,
wholeHeight=document.documentElement.clientHeight||document.body.clientHeight,
point=scrollTop+wholeHeight;//页面底部距离header的距离
vararr=$('.pin');
varlastHei=arr[arr.length-1].getBoundingClientRect().top;
return(lastHei<point)?true:false;
}
//处理滑动
vardealScroll=(function(){
varmain=$('#main')[0],
flag=true;
returnfunction(){
if(isLoad()&&flag){
for(vari=0,data;data=dataInt[i++];){
vardiv=document.createElement('div');
div.innerHTML=temp(data.src);
div.className="pin";
main.appendChild(div);
overLoad(div);//和上面的overload有耦合性质
}
flag=false;
setTimeout(function(){//控制滑行手速,时间越长对速度的滑动时间影响越大。
flag=true;
},200);
}
}
})();
functiontemp(src){
return`
<divclass="box">
<imgsrc="http://cued.xunlei.com/demos/publ/img/P_00${src}"/>
</div>
`;
}
其实,精华就在上一部分,这个只是作为一个加载数据的手段,当然,你可以点击加载(手动触发),或者其他的加载方法。当然,怎么设置完全是取决于你的。所以,随大流,依然是通过下滑滚动加载。继续,找入口函数->dealScroll.该函数执行的任务就是,通过isload函数,判断是否可以进行加载判断。我们看一下isload函数,这个就是滚动加载的关键点.
functionisLoad(){//是否可以进行加载
varscrollTop=document.documentElement.scrollTop||document.body.scrollTop,
wholeHeight=document.documentElement.clientHeight||document.body.clientHeight,
point=scrollTop+wholeHeight;//页面底部距离header的距离
vararr=$('.pin');
varlastHei=arr[arr.length-1].getBoundingClientRect().top;
return(lastHei<point)?true:false;
}
通过计算得出,页面底部距视口的位置(工具条下部)与最后一个元素的绝对位置比较,如果滑动距离超过,则启用加载。
yeah~That'sover.
backtodealScroll
接下来就是看加载的部分了,这个部分其实也没什么说的,就是创建一个div节点,然后将他放到容器的最后,并且通过overLoad函数来处理该节点的位置。另外在该函数的末尾,我设置了一个控制滑动速度的trick,通过对函数的节流,防止有时候,请求过慢,用户重复发送请求,造成资源浪费。
然后,这一部分也可以告一段落了。
响应式
最后一部分就是响应式了,这部分,也超级简单,只要你封装性做的好,其实这一部分就是添加一个resize事件就over了。我们继续找入口函数。
varresize=(function(){
varflag;
returnfunction(fn){
clearTimeout(flag);
flag=setTimeout(function(){//函数的节流,防止用户过度移动
fn();
console.log("ok")
},500);
}
})();
同样,这里使用到了函数节流的思想,要知道,作为一个程序员,永远不要以为用户干不出什么傻事.比如,没事的时候拖着浏览器窗口玩,放大,缩小,再放大...其实,这事我经常干,因为没有女朋友,写代码写累了,就拖浏览器玩。所以,考虑到我们单身狗的需求,使用函数节流是非常有必要的。感兴趣的童鞋,可以参考我前面的文章,进行学习。说明一下,这里的回调函数指的就是init函数,但是需要对init做些改动。详见。
varupdate=function(ele){//当resize的时候,重新设置
ele.style.position="initial";
}
//初始化时执行函数
varinit=function(){
varpins=$(".pin"),
col=columns();
arrHeight=[];//清空高度
setCenter();//设置包裹容器的宽度
for(vari=0,pin;pin=pins[i];i++){
if(i<col){//存储第一排的高度
arrHeight.push(pin.offsetHeight);
update(pin);
}else{
overLoad(pin);//将元素的位置重排
}
}
}
上面需要加入update,对新的第一排元素进行更新。
然后就可以直接搬过来使用即可。
这就是绝对是布局的大部分内容了。
总结一下吧,这个部分我应该是,重构Amy的例子的,总的来说是遵循了,最少知识原则的,一个函数尽量不带参数,就一定不带参数了.还有就是一个函数的书写,不超过33行,并且尽量的实现细粒度表达,实现函数的重用。当然,我写的这一部分还是有很多问题的,js组织结构还是比较混论的,一些hack直接与主要业务逻辑放在一起。我也相信,读者们肯定有自己独特的见解,写出来的代码,肯定比我好太多!!!(这个是肯定的.)
列式布局
同样,先看demo.
https://jsfiddle.net/jimmyTHR/amyvoagm/5/
我们对比一下上面的,绝对式布局。一个很大的却别就是,页面的计算量大大降低。什么意思呢?即,页面重绘的次数减少,操作节点数减少,页面布局更加清晰。
首先我们来看一下他的html内容。我这里就不贴出全部了,同样,只列出一个基本单元。
<divid="container"> <spanclass="column"> <spanclass="panel"> <imgsrc="http://cued.xunlei.com/demos/publ/img/P_009.jpg"alt=""> <p></p> </span> </span> </div>
上面就是一个基本的单元,需要注意的是,我们需要设置一个column表示一列。而span.panel就是一个基本砖块。
好了,不在这地方磨叽了。我们再来看一下scss.
$font:10px/1.5sans-serif,"MicrosoftYaHei","Arial";
html,body{
height:100%;
width:100%;
font:$font;
background-color:#eee;
}
#container{
width:100%;
height:100%;
position:relative;
text-align:center;
}
.column{
display:inline-block;
width:210px;
vertical-align:top;
padding:010px;
.panel{
padding:10px010px0;
display:block;
text-align:center;
margin-bottom:10px;
border:1pxsolid#ccc;
background-color:#fff;
text-decoration:none;
}
}
这我也不磨叽了,scss这里需要引入normalize.scss进行和谐处理。
html和css介绍完了,我们要开始搬砖了。
JS原理
上面说了,列式布局简直算是完虐绝对式布局.绝对式布局,简直就像10元/天的搬砖工。而列式布局就是站在那看他搬砖的监工。同样都是搬砖的,一个卖苦力,一个秀智商。简直了!!!
听了逼逼,我们来直面一下惨淡的人生。
列式布局的原理其实和绝对式布局没有太大的却别。
同样也有3个部分,一是页面加载自适应,二是滑动加载,三是响应式布局。
分别讲解:
加载自适应
我们先看一下代码吧:
var$=function(){//一个hacks
returndocument.querySelectorAll.apply(document,arguments);
}
varwaterFall=(function(){
//初始化布局
vararrHeight=[];//列的高度
varcolumns=function(){//计算页面最多可以放多少列
varbodyW=$('#container')[0].clientWidth,
pinW=$(".column")[0].offsetWidth;
returnMath.floor(bodyW/pinW);
}
//设置瀑布流居中
vargetHtml=function(){
varcols=$('.column'),//获得已有的列数
arrHtml=[];
for(vari=0,col;col=cols[i++];){
varhtmls=col.innerHTML.match(/<img(?:.|\n|\r|\s)*?p>/gi);//获取一个columns的
arrHtml=arrHtml.concat(htmls);
}
returnarrHtml;
}
//获得数组中最低的高度
vargetMinIndex=function(){//获得最小高度的index
varminHeight=Math.min.apply(null,arrHeight);//获得最小高度
for(variinarrHeight){
if(arrHeight[i]===minHeight){
returni;
}
}
}
varcreateCol=function(){
varcols=columns(),//获得列数
contain=$("#container")[0];
contain.innerHTML='';//清空数据
for(vari=0;i<cols;i++){
varspan=document.createElement("span");
span.className="column";
contain.appendChild(span);
}
}
//初始化列的高度
varinitHeight=function(){
varcols=columns(),
arr=[];
for(vari=0;i<cols;i++){
arr.push(0);
}
arrHeight=arr;
}
//创建一个ele并且添加到最小位置
varcreateArticle=function(html){
varcols=$('.column'),
index=getMinIndex(),
ele=document.createElement('article');
ele.className="panel";;
ele.innerHTML=html;
cols[index].appendChild(ele);
arrHeight[index]+=ele.clientHeight;
}
//遍历获得的html然后添加到页面中
varreloadImg=function(htmls){
for(vari=0,html,index;html=htmls[i++];){
createArticle(html);
}
}
varonload=function(){
varcontain=$("#container")[0],//获得容器
arrHtml=getHtml();//获得现有的所有瀑布流块
//创建列,然后进行加载
createCol();
//初始化arrHeight
initHeight();
//进行页面的重绘
reloadImg(arrHtml);
returnthis;
}
})();
看见一个程序,先找他的入口函数,显然,一开始应该是onload,那么,观察onload函数.可以发现,里面一共有4个函数.
由于用户的宽度不确定,所以我们的列数也不是一定的。这时候,就需要获取实际尺寸然后进行一个计算才行。然后需要对原有的数据,进行重排。
所以,getHtml就是来获取一开始的原有数据(innerHTML);
然后就可以更具宽度来添加列了。
createCol函数就是更具宽度来添加列的。
这时候,我们需要一个数组(arrHeight)来保存每列的高(默认都为0).
然后就可以进行页面重排=>reloadImg(arrHtml),arrHtml就是原始数据。
好,我们这里初级搬砖完成。
接下来,是要开始加固了。
滑动加载
这个应该算是我直接copy过来的,所以说,函数写的好,重用性也是棒棒哒。
showucode
varisLoad=function(){//是否可以进行加载
varscrollTop=document.documentElement.scrollTop||document.body.scrollTop,
wholeHeight=document.documentElement.clientHeight||document.body.clientHeight,
point=scrollTop+wholeHeight;//页面底部距离header的距离
varlastHei=Math.min.apply(null,arrHeight);
return(lastHei<point)?true:false;
}
vardealScroll=(function(){
window.onscroll=()=>{dealScroll();}
varcontainer=$('#container')[0];
returnfunction(){
if(isLoad()){
for(vari=0,html,data;data=dataInt[i++];){
html=tpl.temp(data.src);//获得数据然后添加模板
createArticle(html);
}
}
returnthis;
}
})();
同样的isload,同样的dealScroll的逻辑。这里需要说明的就是,createArticle就是给最低高度列添加砖块的函数。
然后,就没有然后啦.
响应式布局
这个我也是直接copy过来的。
varresize=(function(){
window.onresize=()=>{resize();};
varflag;
returnfunction(){
clearTimeout(flag);
flag=setTimeout(()=>{onload();},500);
returnthis;
}
需要说明的是,onload,dealScroll,resize这3个函数,后面我都加上"returnthis".目的是可以进行链式调用,以备后面重用性的需要。
最后说两句
响应式布局的流行真的很突然,慢慢的,慢慢的我没有防备。感觉在11年和12年的时候简直就是一种风尚,而现在回味过来,看这个布局方法,感觉精髓依在的。我们也可以进行变通,我们可以往右滑,右边节点增加。获得抖抖手机,节点增加,这些都是可以完成的。(因为主要的逻辑我们已经有了,只是判断增加的方式变了而已)。
我这里写的一部分内容,应该算是(云网页)吧。大家有兴趣可以直接拷贝到自己的IDE或者Sublime里面进行测试,数据都是放在云端的。
大家如果真的对前端有兴趣的话,造轮子就是一个最好的办法。就像我们小时候做计算题,事实上这些题目早就有了,已经有无数人做过。那我们做还有什么意义呢?如果你这样想的话,你已经把你自己给平庸化了(不是平凡化)。你抱着的是一个,这个别人做过了,我做了又没有什么用,还没有别人做的好,我不想做。。。但是,有没有想过,为什么你叫这个名字,你为什么在这个城市长大,为什么能看到我写的这篇文章。因为道理很简单,你就是一个很独特的人,你做的任何事情都是不可复制的。当你写瀑布流的时候,你浏览的网站,看过的资料,写的代码。这些串联起来都是你最独特的flag。所以,造轮子不是waste而恰恰相反是worthy,同样就是codereview一样。只有自己做过才知道,这个东西,我真的知道。