七夕节见男神,爱TA就送:JS版冒泡排序动画!
一眨眼的工夫,很快就又到了新一年的七夕节了,正好碰到今天公司搞了一个七夕小活动的工夫,就用JS写了一个冒泡排序算法,顺便写了动画排序过程。
其实这种算法动画效果网上有很多例子,但今天兴趣使然,就抽空想自己实现一下。
代码的优化这里就不做讨论了,完全是为了实现自己小小的满足感,感兴趣的童鞋可以自己在电脑上做一下代码优化。
下面是效果图(部分):
01
动态渲染移动元素
废话不多说,直接上代码(大佬会做的比我更好):
// 渲染容器
<div id="container"></div>
// 要排序的数组
let arr = [27, 37, 2, 50, 10, 5, 41, 12, 41, 6, 4, 24, 47, 31, 12];
// 每个dom元素的左边距单位
let posLeft = 57;
// 获取渲染容器
const container = document.getElementById('container');
/**
* @description 动态渲染移动元素
* @param { Object } elem 渲染容器
* @param { Array } arr 数据列表
* @return { Void }
*/
const renderHTML = (elem, arr) => {
let html = '',
className = '',
totalWidth = 0;
arr.forEach((item, index) => {
if (item * 3 < 18) className = 'out';
else className = '';
totalWidth = index;
html += `<li class="item" data-index="${ index }" style="height: ${ item * 3 }px; left: ${ posLeft * index }px;">
<span class="${ className }">${ item }</span>
</li>`;
});
elem.innerHTML = `<ul class="list" style="width: ${ posLeft * totalWidth + 48 }px;">${ html }</ul>`;
}
renderHTML(container, arr);
html, body {
margin: 0;
padding: 0;
}
#container,
h1 {
height: 100%;
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(173, 216, 230);
font-size: 20px;
color: #333;
pointer-events: none;
transition: all .3s ease;
position: absolute;
}
.item:last-child {
margin-right: 0;
}
.item.left,
.item.right {
background-color: rgb(0, 128, 0);
}
.item.left::before,
.item.right::before {
content: "";
position: absolute;
left: -4px;
top: -4px;
width: calc(100% + 4px);
height: calc(100% + 4px);
border: 2px solid red;
}
.item.left::after,
.item.right::after {
content: "";
position: absolute;
left: 50%;
bottom: -20px;
width: 0;
height: 0;
border: 10px solid transparent;
border-bottom-color: red;
transform: translateX(-50%);
}
.item.success {
background-color: rgb(255, 165, 0);
}
.out {
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
color: #333;
}
在上面的代码中,className = 'out'
是用来判断数字是否已超出元素内部,如果超出,则显示在元素外部,而非内部。防止字体与元素显示交叉感。
${ item * 3 }
,乘以3完全是为了元素看起来更高大一些,要不然,当要排序的数值中包括<10
的数字时,元素的高度就会几乎看不见。
${ posLeft * index }
,会随着元素的渲染,left
值会随着index
索引值的增大而改变。
-
57 * 0 = 0 -
57 * 1 = 57 -
57 * 2 = 114 -
...
${ posLeft * totalWidth + 48 }
,根据子元素渲染个数设置父元素的总宽度,因为子元素采用了定位布局,所以父元素撑不开,需要动态设置宽度,以适配页面的居中显示。
当然不设置父元素的宽度也不影响执行,只是会在一定程度上,列表会不方便居中在视窗的水平垂直居中位置。
此时已经渲染出列表了,但是没有执行排序算法,下面添加排序算法。
02
冒泡排序算法
const bubbleSort = arr => {
const len = arr.length;
for (let i = 0; i < len; i ++) {
for (let j = i + 1; j < len; j ++) {
if (arr[i] > arr[j]) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
return arr;
}
// 原数组:[27, 37, 2, 50, 10, 5, 41, 12, 41, 6, 4, 24, 47, 31, 12]
bubbleSort(arr);
冒泡排序算法有很多种方法,这里只介绍了一种,就是:套用两层循环,一个一个对比,如果找到符合的元素,就通过[arr[i], arr[j]] = [arr[j], arr[i]]
数组解构的方式,调换两个元素的位置。
也许这样表达大家有些难以理解,当然高手飘过哈。其实我也当初不理解,不过通过自己摸索再结合动画的形式来看,对理解算法的过程会更加明确。
结果是已经排好了,但是还不能让它们动起来,想要动起来,继续和我一步一步做下去。
03
让元素动起来
想要让元素动起来前,我们需要有两个标识,一个左一个右。
左称为:.item.left 左边界元素
。右称为:.item.right 右边界元素
。我在这里用了这两个元素,完全是为了在执行动画的时候方便区分理解。
代表:左边界元素需要每次和右边界元素去对比,如果有小于左元素的,则进行调换,否则不动。
比如这样:
动的一直是右边界,而非左边界,左边界在这里只是充当一个基点的角色,用来和其它元素进行对比。
改造一下:
const list = document.getElementsByClassName('list')[0];
// 这里需要扩展字符串把元素列表扩展成真正的dom数组,否则不能进行下面的filter语法
const items = [...document.getElementsByClassName('item')];
const setPos = (left, right) => {
// 获取左、右边界元素
let eleLeft = items.filter(item => item.getAttribute('data-index') == left);
let eleRight = items.filter(item => item.getAttribute('data-index') == right);
let leftInfo = {
pos: eleLeft[0].offsetLeft,
index: eleLeft[0].getAttribute('data-index')
};
let rightInfo = {
pos: eleRight[0].offsetLeft,
index: eleRight[0].getAttribute('data-index')
};
// 设置左、右边界元素的距离与高亮
// 因为要互换位置,所以class类名要互相调换
eleLeft[0].style.left = rightInfo.pos + 'px';
eleLeft[0].className = 'item right';
eleRight[0].style.left = leftInfo.pos + 'px';
eleRight[0].className = 'item left';
}
// 小于左边界索引的元素,全部设置成高亮,代表已经排序完成的元素
const setSuccess = (arr, index) => {
for (let i = 0, len = arr.length; i < len; i ++) {
if (i < index) arr[i].className = 'item success';
}
}
// type未传值,或,className包含right时,清空所有高亮
const clearClass = type => {
for (let i = 0, len = items.length; i < len; i ++) {
if (!type || items[i].className.includes(type)) {
items[i].className = 'item';
break;
}
}
}
const bubbleSort = arr => {
const len = arr.length;
for (let i = 0; i < len; i ++) {
// 重新获取列表元素
const items = document.getElementsByClassName('item');
// 清空样式
clearClass();
// 设置完成排序的高亮元素
setSuccess(items, i);
items[i].className = 'item left';
if (!items[i + 1]) {
// 如果后面已经没有元素了,则停止排序,完成操作,高亮最后一个元素
setSuccess(items, i + 1);
break;
}
// 依次用左边界元素对比右界所有元素
for (let j = i + 1; j < len; j ++) {
// 只清空包含右边界元素的高亮
clearClass('right');
items[j].className = 'item right';
// 如果左边界比右边界大
if (arr[i] > arr[j]) {
// 则调换两个元素的位置
setPos(i, j);
// 调换数组中两个值的位置
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
}
此时刷新页面,好吧,我傻眼了。。。都乱了。
因为我在上面的CSS
中加了300毫秒的元素动画时间transition: all .3s ease
。
但是循环太快了,还来不及做动画,元素在运动的过程中又再次执行下次渲染,就造成了这种无脑局面。。。
想要解决这个问题,需要有一种可以让循环慢下来的办法才行,下面我们可以这样做,想让它多慢,就有多慢。
const sleep = time => {
return new Promise(resolve => setTimeout(resolve, time));
}
const bubbleSort = async arr => {
const len = arr.length;
for (let i = 0; i < len; i ++) {
// 重新获取列表元素
const items = document.getElementsByClassName('item');
// 清空样式
clearClass();
// 设置完成排序的高亮元素
setSuccess(items, i);
items[i].className = 'item left';
// 隔600毫秒后再执行后续操作
await sleep(600);
if (!items[i + 1]) {
// 如果后面已经没有元素了,则停止排序,完成操作,高亮最后一个元素
setSuccess(items, i + 1);
break;
}
// 依次用左边界元素对比右界所有元素
for (let j = i + 1; j < len; j ++) {
// 只清空包含右边界元素的高亮
clearClass('right');
items[j].className = 'item right';
// 隔600毫秒
await sleep(600);
// 如果左边界比右边界大
if (arr[i] > arr[j]) {
// 则调换两个元素的位置
setPos(i, j);
// 调换数组中两个值的位置
[arr[i], arr[j]] = [arr[j], arr[i]];
// 保证移动动画执行完成后,再次进行下一轮比较
await sleep(800);
}
}
}
}
我们可以使用async
与promise
语法搭配来模拟异步操作过程,在上面的例子中,循环的过程必须要等到sleep
方法有返回值后,才可以进行后面的循环,采用传入的时间参数毫秒
,来生成一个设置在时间范围内的定时器,直至等待返回。
这是怎么回事,虽然在有条理的做着动画,但是明显不对吧。都乱套了。。。
const setPos = (left, right) => {
// ...
// 需要加个延迟,等动画特效执行完成后,再去设置调换元素的索引值与实际位置
setTimeout(() => {
// 因为此时元素的位置已经改变,所以需要重新获取元素列表
const items = 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]);
}, 400);
}
那是因为运动元素动画完成后,只是视图上更新而已,视觉上看确实变了,但变的是元素的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其实哪有完美的,如果追求完美,需要优化的地方还有很多,比如:代码复用性
、代码不简洁
、命名是否规范
、兼容性是否可行
等等。
感兴趣的小伙伴可以自己去试试做下优化,我相信你们肯定比我强。
End
最后
感谢您抽出宝贵的时间阅读本文,希望对您有所帮助。
如果您遇到什么疑问或者建议,欢迎多多交流,大家共同进步。
在阅读过程中,如果有不正确的地方,希望您能提出来,我会努力改正并提供更优质的文章。
结语
-
关注后回复 资料
免费领取学习资料 -
关注后回复 进群
拉你进技术交流群 -
关注后回复 福利
查看最新、即将开展的福利活动,先到先得 -
欢迎关注 鲸选派
,更多「福利干货」
及时推送