一眨眼的工夫,很快就又到了新一年的七夕节了,正好碰到今天公司搞了一个七夕小活动的工夫,就用JS写了一个冒泡排序算法,顺便写了动画排序过程。
其实这种算法动画效果网上有很多例子,但今天兴趣使然,就抽空想自己实现一下。
代码的优化这里就不做讨论了,完全是为了实现自己小小的满足感,感兴趣的童鞋可以自己在电脑上做一下代码优化。
下面是效果图(部分):
动态渲染移动元素
废话不多说,直接上代码(大佬会做的比我更好):
//渲染容器divid="container"/div
/要排序的数组letarr=[27,37,2,50,10,5,41,12,41,6,4,24,47,31,12];//每个dom元素的左边距单位letposLeft=57;//获取渲染容器constcontainer=document.getElementById(container);/***
description动态渲染移动元素*param{Object}elem渲染容器*param{Array}arr数据列表*return{Void}*/constrenderHTML=(elem,arr)={lethtml=,className=,totalWidth=0;arr.forEach((item,index)={if(item*)className=out;elseclassName=;totalWidth=index;html+=`liclass="item"data-index="${index}"style="height:${item*3}px;left:${posLeft*index}px;"spanclass="${className}"${item}/span/li`;});elem.innerHTML=`ulclass="list"style="width:${posLeft*totalWidth+48}px;"${html}/ul`;}renderHTML(container,arr);html,body{margin:0;padding:0;}#container,h1{height:%;display:flex;justify-content:center;align-items:center;}h1{height:auto;margin:6%0;}.list{position:relative;display:flex;align-items:flex-end;list-style:none;padding:0;margin:0;margin-top:10%;}.item{width:45px;margin-right:12px;display:inline-flex;justify-content:center;align-items:center;background-color:rgb(,,);font-size:20px;color:#;pointer-events:none;transition:all.3sease;position:absolute;}.item:last-child{margin-right:0;}.item.left,.item.right{background-color:rgb(0,,0);}.item.left::before,.item.right::before{content:"";position:absolute;left:-4px;top:-4px;width:calc(%+4px);height:calc(%+4px);border:2pxsolidred;}.item.left::after,.item.right::after{content:"";position:absolute;left:50%;bottom:-20px;width:0;height:0;border:10pxsolidtransparent;border-bottom-color:red;transform:translateX(-50%);}.item.success{background-color:rgb(,,0);}.out{position:absolute;bottom:calc(%+2px);left:50%;transform:translateX(-50%);color:#;}
在上面的代码中,className=out是用来判断数字是否已超出元素内部,如果超出,则显示在元素外部,而非内部。防止字体与元素显示交叉感。
${item*3},乘以3完全是为了元素看起来更高大一些,要不然,当要排序的数值中包括10的数字时,元素的高度就会几乎看不见。
${posLeft*index},会随着元素的渲染,left值会随着index索引值的增大而改变。
57*0=*1=*2=...
${posLeft*totalWidth+48},根据子元素渲染个数设置父元素的总宽度,因为子元素采用了定位布局,所以父元素撑不开,需要动态设置宽度,以适配页面的居中显示。
当然不设置父元素的宽度也不影响执行,只是会在一定程度上,列表会不方便居中在视窗的水平垂直居中位置。
此时已经渲染出列表了,但是没有执行排序算法,下面添加排序算法。
冒泡排序算法
constbubbleSort=arr={constlen=arr.length;for(leti=0;ilen;i++){for(letj=i+1;jlen;j++){if(arrarr[j]){[arr,arr[j]]=[arr[j],arr];}}}returnarr;}//原数组:[27,37,2,50,10,5,41,12,41,6,4,24,47,31,12]bubbleSort(arr);
冒泡排序算法有很多种方法,这里只介绍了一种,就是:套用两层循环,一个一个对比,如果找到符合的元素,就通过[arr,arr[j]]=[arr[j],arr]数组解构的方式,调换两个元素的位置。
也许这样表达大家有些难以理解,当然高手飘过哈。其实我也当初不理解,不过通过自己摸索再结合动画的形式来看,对理解算法的过程会更加明确。
结果是已经排好了,但是还不能让它们动起来,想要动起来,继续和我一步一步做下去。
让元素动起来
想要让元素动起来前,我们需要有两个标识,一个左一个右。
左称为:.item.left左边界元素。右称为:.item.right右边界元素。我在这里用了这两个元素,完全是为了在执行动画的时候方便区分理解。
代表:左边界元素需要每次和右边界元素去对比,如果有小于左元素的,则进行调换,否则不动。
比如这样:
动的一直是右边界,而非左边界,左边界在这里只是充当一个基点的角色,用来和其它元素进行对比。
改造一下:
constlist=document.getElementsByClassName(list)[0];//这里需要扩展字符串把元素列表扩展成真正的dom数组,否则不能进行下面的filter语法constitems=[...document.getElementsByClassName(item)];constsetPos=(left,right)={//获取左、右边界元素leteleLeft=items.filter(item=item.getAttribute(data-index)==left);leteleRight=items.filter(item=item.getAttribute(data-index)==right);letleftInfo={pos:eleLeft[0].offsetLeft,index:eleLeft[0].getAttribute(data-index)};letrightInfo={pos:eleRight[0].offsetLeft,index:eleRight[0].getAttribute(data-index)};//设置左、右边界元素的距离与高亮//因为要互换位置,所以class类名要互相调换eleLeft[0].style.left=rightInfo.pos+px;eleLeft[0].className=itemright;eleRight[0].style.left=leftInfo.pos+px;eleRight[0].className=itemleft;}//小于左边界索引的元素,全部设置成高亮,代表已经排序完成的元素constsetSuccess=(arr,index)={for(leti=0,len=arr.length;ilen;i++){if(iindex)arr.className=itemsuccess;}}//type未传值,或,className包含right时,清空所有高亮constclearClass=type={for(leti=0,len=items.length;ilen;i++){if(!type
items.className.includes(type)){items.className=item;break;}}}constbubbleSort=arr={constlen=arr.length;for(leti=0;ilen;i++){//重新获取列表元素constitems=document.getElementsByClassName(item);//清空样式clearClass();//设置完成排序的高亮元素setSuccess(items,i);items.className=itemleft;if(!items[i+1]){//如果后面已经没有元素了,则停止排序,完成操作,高亮最后一个元素setSuccess(items,i+1);break;}//依次用左边界元素对比右界所有元素for(letj=i+1;jlen;j++){//只清空包含右边界元素的高亮clearClass(right);items[j].className=itemright;//如果左边界比右边界大if(arrarr[j]){//则调换两个元素的位置setPos(i,j);//调换数组中两个值的位置[arr,arr[j]]=[arr[j],arr];}}}}
此时刷新页面,好吧,我傻眼了。。。都乱了。
因为我在上面的CSS中加了毫秒的元素动画时transition:all.3sease。
但是循环太快了,还来不及做动画,元素在运动的过程中又再次执行下次渲染,就造成了这种无脑局面。。。
想要解决这个问题,需要有一种可以让循环慢下来的办法才行,下面我们可以这样做,想让它多慢,就有多慢。
constsleep=time={returnnewPromise(resolve=setTimeout(resolve,time));}constbubbleSort=asyncarr={constlen=arr.length;for(leti=0;ilen;i++){//重新获取列表元素constitems=document.getElementsByClassName(item);//清空样式clearClass();//设置完成排序的高亮元素setSuccess(items,i);items.className=itemleft;//隔毫秒后再执行后续操作awaitsleep();if(!items[i+1]){//如果后面已经没有元素了,则停止排序,完成操作,高亮最后一个元素setSuccess(items,i+1);break;}//依次用左边界元素对比右界所有元素for(letj=i+1;jlen;j++){//只清空包含右边界元素的高亮clearClass(right);items[j].className=itemright;//隔毫秒awaitsleep();//如果左边界比右边界大if(arrarr[j]){//则调换两个元素的位置setPos(i,j);//调换数组中两个值的位置[arr,arr[j]]=[arr[j],arr];//保证移动动画执行完成后,再次进行下一轮比较awaitsleep();}}}}
我们可以使用async与promise语法搭配来模拟异步操作过程,在上面的例子中,循环的过程必须要等到sleep方法有返回值后,才可以进行后面的循环,采用传入的时间参数毫秒,来生成一个设置在时间范围内的定时器,直至等待返回。
这是怎么回事,虽然在有条理的做着动画,但是明显不对吧。都乱套了。。。
constsetPos=(left,right)={//...//需要加个延迟,等动画特效执行完成后,再去设置调换元素的索引值与实际位置setTimeout(()={//因为此时元素的位置已经改变,所以需要重新获取元素列表constitems=document.getElementsByClassName(item);//设置左边界元素的索引值为右边界元素的索引值eleLeft[0].setAttribute(data-index,rightInfo.index);//把左边界元素插入到右边界元素的下一个兄弟元素的前面list.insertBefore(eleLeft[0],items[right].nextElementSibling);//设置右边界元素的索引值为左边界元素的索引值eleRight[0].setAttribute(data-index,leftInfo.index);//把右边界元素插入到左边界元素的前面list.insertBefore(eleRight[0],items[left]);},);}
那是因为运动元素动画完成后,只是视图上更新而已,视觉上看确实变了,但变的是元素的left值,而非真实的dom列表位置。
所以我们需要在每次调换元素位置后,需要重新获取一下dom列表,因为此时的元素位置已经发生变化,需要重新更新此列表。
然后再设置一下调换元素的索引值,保证新设置的索引和调换后的索引是一一对应的。
最后需要list.insertBefore(eleLeft[0],items[right].nextElementSibling);,把左边界元素,插入到右边界下一个兄弟元素的前面。
if(!items[i+1]){//如果后面已经没有元素了,则停止排序,完成操作,高亮最后一个元素setSuccess(items,i+1);break;}
在循环的时候,我这里做了判断后面是否还有其它元素。
如果没有,则不会执行调换元素的函数,否则当循环到最后一个元素,后面再没有元素的时候,执行items[right].nextElementSibling会报错。list.insertBefore(eleRight[0],items[left]);,把右边界元素插入到左边界元素的前面。这样再看的话,是不是就没有问题啦!其实这个demo其实哪有完美的,如果追求完美,需要优化的地方还有很多,比如:代码复用性、代码不简洁、命名是否规范、兼容性是否可行等等。感兴趣的小伙伴可以自己去试试做下优化,我相信你们肯定比我强。最后感谢您抽出宝贵的时间阅读本文,希望对您有所帮助。
如果您遇到什么疑问或者建议,欢迎多多交流,大家共同进步。
在阅读过程中,如果有不正确的地方,希望您能提出来,我会努力改正并提供更优质的文章。