深入探秘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一样。只有自己做过才知道,这个东西,我真的知道。