最近做的一个小需求涉及到排序,界面如下所示:
因为项目是使用vue的,所以实现方式很简单,视图部分不用管,本质上就是操作数组,代码如下:
{
// 上移
moveUp (i) {
// 把位置i的元素移到i-1上
let tmp = this.form.replayList.splice(i, 1)
this.form.replayList.splice(i - 1, 0, tmp[0])
},
// 下移
moveDown (i) {
// 把位置i的元素移到i+1上
let tmp = this.form.replayList.splice(i, 1)
this.form.replayList.splice(i + 1, 0, tmp[0])
}
}
这样就可以正常的交换位置了,但是是突变的,没有动画,所以不明显,于是一个码农的自我修养(实际上是太闲)让我打开了vue的网站,看到了这个示例:https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%97%E8%A1%A8%E7%9A%84%E6%8E%92%E5%BA%8F%E8%BF%87%E6%B8%A1
这个示例我已看过多遍,但是一直没用过,这里刚好就是我要的效果,于是一通复制粘贴大法:
<template>
<transition-group name="flip-list" tag="p">
<!--循环生成列表部分,略-->
</transition-group>
</template>
<style>
.flip-list-move {
transition: transform 0.5s;
}
</style>
这样就有交换的过渡效果了,如下:
嗯,舒服了很多,这个需求到这里就完了,但是事情并没有结束,我突然想到了以前看一些算法文章的时候通常会配上一些演示的动画,感觉跟这个很类似,那么是不是可以用这个来实现呢,当然是可以的。
先写一下基本的布局和样式:
<template>
<div class="sortList">
<transition-group name="flip-list" tag="p">
<div
class="item"
v-for="item in list"
:key="item.index"
:style="{height: (item.value / max * 100) + '%'}"
>
<span class="value">{{item.value}}</span>
</div>
</transition-group>
</div>
</template>
<style>
.flip-list-move {
transition: transform 0.5s;
}
</style>
list
是要排序的数组,当然是经过处理的,在真正的源数组上加上了唯一的index
,因为要能正常过渡的话列表的每一项需要一个唯一的key:
const arr = [10, 43, 23, 65, 343, 75, 100, 34, 45, 3, 56, 22]
export default {
data () {
return {
list: arr.map((item, index) => {
return {
index,
value: item
}
})
}
}
}
max
是这个数组中最大的值,用来按比例显示高度:
{
computed: {
max () {
let max = 0
arr.forEach(item => {
if (item > max) {
max = item
}
})
return max
}
}
}
其他样式可以自行发挥,显示效果如下:
简约而不简单~,现在万事俱备,只欠让它动起来,排序算法有很多,但是本人比较菜,所以就拿冒泡算法来举例,最最简单的冒泡排序算法如下:
{
mounted(){
this.bubbleSort()
},
methods: {
bubbleSort() {
let len = this.list.length
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - i - 1; j++) {
if (this.list[j] > this.list[j + 1]) { // 相邻元素两两对比
let tmp = this.list[j] // 元素交换
this.$set(this.list, j, this.list[j + 1])
this.$set(this.list, j + 1, tmp)
}
}
}
}
}
}
但是这样写它是不会动的,瞬间就给你排好了:
试着加个延时:
{
mounted () {
setTimeout(() => {
this.bubbleSort()
}, 1000)
}
}
刷新看效果:
有动画了,不过这种不是我们要的,我们要的应该是下面这样的才对:
所以来改造一下,因为for循环是只要开始执行就不会停的,所以需要把两个for循环改成两个函数,这样可以控制每个循环什么时候执行:
{
bubbleSort () {
let len = this.list.length
let i = 0
let j = 0
// 内层循环
let innerLoop = () => {
// 每个内层循环都执行完毕后再执行下一个外层循环
if (j >= (len - 1 - i)) {
j = 0
i++
outLoop()
return false
}
if (this.list[j].value > this.list[j + 1].value) {
let tmp = this.list[j]
this.$set(this.list, j, this.list[j + 1])
this.$set(this.list, j + 1, tmp)
}
// 动画是500毫秒,所以每隔800毫秒执行下一个内层循环
setTimeout(() => {
j++
innerLoop()
}, 800)
}
// 外层循环
let outLoop = () => {
if (i >= len) {
return false
}
innerLoop()
}
outLoop()
}
}
这样就实现了每一步的动画效果:
但是这样不太直观,因为有些相邻不用交换的时候啥动静也没有,不知道当前具体排到了哪两个,所以需要突出当前正在比较交换的两个元素,首先模板部分给当前正在比较的元素加一个类名,用来高亮显示:
<div
:class="{sortingHighlight: sorts.includes(item.index)}"
>
<span class="value">{{item.value}}</span>
</div>
js部分定义一个数组sorts
来装载当前正在比较的两个元素的唯一的index
值:
{
data() {
return {
sorts: []
}
},
methods: {
bubbleSort () {
// ...
// 内层循环
let innerLoop = () => {
// 每个内层循环都执行完毕后再执行下一个外层循环
if (j >= (len - 1 - i)) {
// 清空数组
this.sorts = []
j = 0
i++
outLoop()
return false
}
// 将当前正在比较的两个元素的index装到数组里
this.sorts = [this.list[j].index, this.list[j + 1].index]
// ...
}
// 外层循环
// ...
}
}
}
修改后效果如下:
最后,再参考刚才别人的示例把已排序的元素也加上高亮:
{
data() {
return {
sorted: []
}
},
methods: {
bubbleSort () {
// ...
// 内层循环
let innerLoop = () => {
// 每个内层循环都执行完毕后再执行下一个外层循环
if (j >= (len - 1 - i)) {
this.sorts = []
// 看这里,把排好的元素加到数组里就ok了
this.sorted.push(this.list[j].index)
j = 0
i++
outLoop()
return false
}
// ...
}
// 外层循环
// ...
}
}
}
最终效果如下:
接下来看一下选择排序,这是选择排序的算法:
{
selectSort() {
for (let i = 0; i < len - 1; i++) {
minIndex = i
for (let j = i + 1; j < len; j++) {
if (this.list[j].value < this.list[minIndex].value) {
minIndex = j
}
}
tmp = this.list[minIndex]
this.$set(this.list, minIndex, this.list[i])
this.$set(this.list, i, tmp)
}
}
}
选择排序涉及到一个当前最小元素,所以需要新增一个高亮:
<div
:class="{minHighlight: min === item.index , sortingHighlight: sorts.includes(item.index), sortedHighlight: sorted.includes(item.index)}"
>
<span class="value">{{item.value}}</span>
</div>
{
data () {
return {
min: 0
}
},
methods: {
selectSort () {
let len = this.list.length
let i = 0; let j = i + 1
let minIndex, tmp
// 内层循环
let innerLoop = () => {
if (j >= len) {
// 高亮最后要交换的两个元素
this.sorts = [this.list[i].index, this.list[minIndex].index]
// 延时是用来给高亮一点时间
setTimeout(() => {
// 交换当前元素和比当前元素小的元素的位置
tmp = this.list[minIndex]
this.$set(this.list, minIndex, this.list[i])
this.$set(this.list, i, tmp)
this.sorted.push(this.list[i].index)
i++
j = i + 1
outLoop()
}, 1000)
return false
}
// 高亮当前正在寻找中的元素
this.sorts = [this.list[j].index]
// 找到比当前元素小的元素
if (this.list[j].value < this.list[minIndex].value) {
minIndex = j
this.min = this.list[j].index
}
setTimeout(() => {
j++
innerLoop()
}, 800)
}
let outLoop = () => {
if (i >= len - 1) {
this.sorted.push(this.list[i].index)
return false
}
minIndex = i
this.min = this.list[i].index
innerLoop()
}
outLoop()
}
}
}
效果如下:
其他的排序也是同样的套路,将for循环或while循环改写成可以控制的函数形式,然后可能需要稍微修改一下显示逻辑,如果你也有打算写排序文章的话现在就可以给自己加上动图展示了!
之前看到这些动图的时候也有想过怎么实现,但是都没有深究,这次业务开发无意中也算找到了其中的一种实现方式,其实核心逻辑很简单,关键是很多时候没有想到可以这么做,这也许是框架带给我们的另一些好处吧。
天我们学习的内容有:过渡,动画,转换,伸缩盒子。
可以说今天学习的内容都是重量级的大佬,学好了,使用css3做出酷炫的效果 So Easy!~~
1.过渡
在css3中,有一个属性可以设置过渡效果。
它就是transition,所谓的过渡效果,指的就是以动画的形式慢慢演化样式属性变化的过程。
A.案例:通过transition设置焦点过渡效果
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>div{width: 200px;height: 200px;margin:200px;background: url(girl.jpg);border-radius:50%;transition:all 1s linear 0.3s;cursor: pointer;}div:hover{box-shadow: 0px 0px 20px blue;}</style></head><body><div></div></body></html>
注意页面中的代码:
第一,我们给div添加了一个hover伪类样式,当我们鼠标悬停在div上方的时候,会给div盒子添加一个蓝色的盒子阴影。
第二,我们给div盒子添加了一个transition样式,设置的值为:all 1s linear 0.3s;
这四个数据分别对应
transition-property(需要过渡的属性):如果设置为all表示所有样式属性都需要过渡。
transition-duration(过渡的时间):以秒作为单位,设置过渡的时间
transition-timing-function(过渡的方式):常用的有linear(匀速),ease(先慢后快),ease-in,ease-out,ease-in-out等
transition-delay(延迟的时间):以秒作为单位进行延迟,延迟之后开始进行过渡效果。
所以,我们通过transition这个复合属性设置的过渡效果为:
all:需要过渡所有的属性
1s:过渡的时间为1秒
linear:匀速过渡
0.3s:在延迟0.3秒之后开始过渡动画。
如果大家理解了上面的描述,那么也就不难理解咱们鼠标放到div上之后,为啥会慢慢出现蓝色的光晕了,就是因为咱们添加了过渡,所以,慢慢的就会给盒子添加阴影效果。
2.动画:
在学习完了过渡之后,发现咱们可以使用transition去以动画的形式展示样式的改变以及变化的过程,这可以帮助我们来实现一些过渡的动画。
但是,有的时候,我们的需求会更加的复杂,要求会更加的多变,那么,transition可能就无法满足我们的需要了,我们需要有更加炫酷,复杂的效果呈现。
那么,动画animation就可以满足我们的需要。
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>@keyframes moveAndChange{0%{left:0px;top:0px;}25%{left:200px;top:200px;background:green;border-radius: 0;}50%{left:400px;top:200px;background:blue;border-radius: 50%;}75%{left:400px;top:0px;background:#ccc;border-radius: 0;}100%{left:0px;top:0px;background:red;border-radius: 50%;}}div{margin:200px;width: 200px;height: 200px;position: absolute;background:red;border-radius:50%;animation: moveAndChange 5s linear 0.5s infinite normal;}</style></head><body><div></div></body></html>
代码效果如下:
同样,让我们来关注编写的代码:
1.在样式中,首先我们使用@keyframes 来定义了一个复杂的动画,在css3中,新增了@keyframes可以来帮助我们添加动画。代码如下:
/*动画的名字叫做moveAndChange*/
@keyframes moveAndChange{
/*动画最初的时候,将left设置为0px,top设置为0px*/
0%{
left:0px;
top:0px;
}
/*当动画进行到25%的时候,使用动画将left过渡到200px,top过渡到200px,
背景颜色过渡为绿色,圆角过渡为0(无圆角)*/
25%{
left:200px;
top:200px;
background:green;
border-radius: 0;
}
/*当动画进行到50%的时候,使用动画将left过渡到400px,top过渡到200px,
背景颜色过渡为蓝色,圆角过渡为50%(正圆)*/
50%{
left:400px;
top:200px;
background:blue;
border-radius: 50%;
}
/*当动画进行到75%的时候,使用动画将left过渡到400px,top过渡到0,
背景颜色过渡为灰色,圆角过渡为0(无圆角)*/
75%{
left:400px;
top:0px;
background:#ccc;
border-radius: 0;
}
/*当动画结束的时候,使用动画将left过渡到0x,top过渡到0px,
背景颜色过渡为红色,圆角过渡为50%(正圆)*/
100%{
left:0px;
top:0px;
background:red;
border-radius: 50%;
}
}
这是一个比较复杂的动画效果,可以发现,它通过百分比的形式将一个完整的动画拆分成了5个部分,每个部分都有不同的样式效果,而当我们采用该动画的元素就会按照设置的顺序和样式效果进行动画的过渡和展示。
2.上面我们只是通过@keyframes创建了一个动画,我们还需要通过特定的语法来使用这个动画。
就是下面这句代码了:
animation: moveAndChange 5s linear 0.5s infinite normal;
它是一个复合属性,设置了6个值,分别对应:
animation-name(设置动画的名称):用来设置动画的名字,我们这里写的是moveAndChange ,也就是说我们就是要使用我们刚刚创建的动画。
animation-duration(设置整个动画的时间):以秒作为单位,我们这里写的是5s,表示整个动画的时间为5秒
animation-timing-function(设置播放动画的方式):播放动画的方式,常用的有linear(匀速),ease(先慢后快),ease-in,ease-out,ease-in-out等,我们使用的是linear匀速播放动画。
animation-delay(设置动画的延迟):以秒作为单位,我们写的是0.5s,表示延迟0.5秒之后开始播放动画。
animation-iteration-count(设置动画播放的次数):播放动画的次数,我们这里写的是infinite ,表示动画将会被播放无限次,如果写数字,那么就会播放数字对应的次数。
animation-direction(设置是否反向播放动画):我们写的是normal,表示正常播放动画,如果写的是
alternate则表示要反向播放动画,大家也可以自己试一试这个效果。
最终,我们通过@keyframes创建动画,通过animation设置动画,成功完成了这个复杂的动画效果。
3.转换
在css3中,我们通过transform属性可以设置元素的转换效果,具体的效果如下:
A.平移
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>body{background:pink;}div{width: 200px;height: 200px;position: absolute;background: green;left:0px;top:0px;transform: translate(300px,300px);}</style></head><body><div></div></body></html>
代码效果如下:
如上图所示,本来div盒子的位置是left:0,top:0;
但是我们通过transform: translate(300px,300px);将盒子进行了偏移,所以,盒子的位置发生了改变。
B.旋转
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>body {background: pink;}div {width: 200px;height: 200px;margin: 200px;position: absolute;background: green;left: 0px;top: 0px;transform: rotate(45deg);}</style></head><body><div></div></body></html>
代码效果如下:
如上图所示,本来div盒子应该是四四方方的。
但是,经过我们的代码transform: rotate(45deg); //deg为单位,表示度数。
进行了45度的旋转之后,呈现出来的就是一个菱形的盒子了,旋转的正方向为顺时针,负方向为逆时针。
C.缩放
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>body {background: pink;}div {width: 200px;height: 200px;margin: 200px;position: absolute;background: green;left: 0px;top: 0px;transform: scale(0.5,0.25);}</style></head><body><div></div></body></html>
代码效果如下:
如上图所示,本来盒子的宽高为200*200,而我们通过transform: scale(0.5,0.25);进行的缩放
scale的第一个参数为0.5,表示横向缩小为0.5倍
scale的第二个参数为0.25,表示纵向缩小为0.25倍。
scale的参数如果为1,则表示不进行任何缩放,小于1就是做缩小,而大于1表示做放大。
小结:transform转换中其实还包含了skew(倾斜),matrix(矩阵转换),相对来说用到的不是特别多,所以在本文中我们便不再做介绍。
4.flex布局
Flex布局,可以简便、完整、响应式地实现各种页面布局。
Flex是Flexible Box的缩写,翻译成中文就是“弹性盒子”,用来为盒装模型提供最大的灵活性。任何一个容器都可以指定为Flex布局。
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>div{box-sizing: border-box;}.parent {width: 600px;height: 200px;margin: 100px;position: absolute;background: green;left: 0px;top: 0px;display: flex;justify-content: flex-start}.parent div{width: 20%;border:1px solid #ccc;background:pink;}</style></head><body><div><div>1</div><div>2</div><div>3</div><div>4</div></div></body></html>
代码效果如下:
如图所示,咱们通过display:flex将.parent元素设置为了flex盒子,那么子元素将会按照justify-content设置的方式进行元素的排列,目前看来,和我们没有设置flex盒子的效果是一致的。
接下来我们更改一下,将justify-content设置为flex-end,效果如下图所示:
所以我们就应该发现,flex-start是让所有的子元素从父元素的左侧开始排列
而flex-end是让所有的子元素从元素的右侧开始排列。
我们再来更改一下,将justify-content设置为center,效果如下图所示:
更厉害了,子元素在父盒子的中央位置排列显示了。
然后,我们再将justify-content设置为space-around,效果如下图所示:
它是平均分配的形式为每一个子元素设置了间距,但是看起来有点变扭。
所以我们推荐将justify-content设置为space-between,效果如下图:
我们还可以通过flex-wrap来设置子元素是否换行显示,以及flex-direction设置子元素排列的顺序。
这两个属性可以设置的值如下:
flex-wrap: nowrap;//不换行,会自动收缩
flex-wrap: warp;//换行,会自动收缩
flex-wrap: warp-reverse;//反转,从默认的从上到下排列反转为从下到上。
flex-direction:row; //从左至右一行一行进行子元素的排列
flex-direction:column; //从上到下一列一列进行子元素的排列
flex-direction:row-reverse; //从右至左一行一行进行子元素的排列
flex-direction:column-reverse; //从下到上一列一列进行子元素的排列
案例代码如下:
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>div{box-sizing: border-box;}.parent {width: 600px;height: 200px;margin: 100px;position: absolute;background: green;left: 0px;top: 0px;display: flex;justify-content: space-between;flex-wrap: nowrap;flex-direction: row-reverse;}.parent div{width: 20%;border:1px solid #ccc;background:pink;}</style></head><body><div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div></div></body></html>
我们设置了flex-wrap: nowrap;(不换行,压缩所有的子元素在一行中显示),以及flex-direction: row-reverse;(反向排列)
代码效果如下:
如果设置为flex-wrap: warp(换行显示无法在一行显示的子元素),则效果如下:
如果将flex-direction: column;,则会纵向排列元素,效果如下图:
除了上面的这些给伸缩盒子父元素设置的样式之外,我们还可以可以伸缩盒子的子元素设置flex属性,用来设置平均分配整个父盒子的空间。
代码如下:
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>div{box-sizing: border-box;}.parent {width: 600px;height: 200px;margin: 100px;position: absolute;background: green;left: 0px;top: 0px;display: flex;justify-content: space-between;}.parent div{flex:1;width: 20%;border:1px solid #ccc;background:pink;}</style></head><body><div><div>1</div><div>2</div><div>3</div><div>4</div></div></body></html>
效果如下:
如上图所示,每个盒子平均分配了父盒子的空间,原本宽度为20%,现在被拉伸了。
除此之外,咱们还可以使用flex属性进行进一步的设置,代码如下:
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Document</title><style>div{box-sizing: border-box;}.parent {width: 600px;height: 200px;margin: 100px;position: absolute;background: green;left: 0px;top: 0px;display: flex;justify-content: space-between;}.parent div:nth-of-type(1){flex:1;border:1px solid #ccc;background:red;}.parent div:nth-of-type(2){flex:2;border:1px solid #ccc;background:green;}.parent div:nth-of-type(3){flex:2;border:1px solid #ccc;background:blue;}.parent div:nth-of-type(4){flex:1;border:1px solid #ccc;background:pink;}</style></head><body><div><div>1</div><div>2</div><div>3</div><div>4</div></div></body></html>
效果如下图:
我们分别给四个子盒子设置了flex:1 , flex:2, flex:2 ,flex:1.
这是什么意思呢?
四个flex加起来一共是6.那么第一个盒子就占据整个父盒子的1/6宽度。
同理,另外三个盒子分别占据2/6,2/6,1/6的宽度,所以就形成了我们现在看到的效果。
原文来源于:黑马程序员社区
学习资源:
想学习css,可以关注:黑马程序员头条号,后台回复:css
眨眼的工夫,很快就又到了新一年的七夕节了,正好碰到今天公司搞了一个七夕小活动的工夫,就用JS写了一个冒泡排序算法,顺便写了动画排序过程。
其实这种算法动画效果网上有很多例子,但今天兴趣使然,就抽空想自己实现一下。
代码的优化这里就不做讨论了,完全是为了实现自己小小的满足感,感兴趣的童鞋可以自己在电脑上做一下代码优化。
下面是效果图(部分):
废话不多说,直接上代码(大佬会做的比我更好):
// 渲染容器
<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索引值的增大而改变。
${ posLeft * totalWidth + 48 },根据子元素渲染个数设置父元素的总宽度,因为子元素采用了定位布局,所以父元素撑不开,需要动态设置宽度,以适配页面的居中显示。
当然不设置父元素的宽度也不影响执行,只是会在一定程度上,列表会不方便居中在视窗的水平垂直居中位置。
此时已经渲染出列表了,但是没有执行排序算法,下面添加排序算法。
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]]数组解构的方式,调换两个元素的位置。
也许这样表达大家有些难以理解,当然高手飘过哈。其实我也当初不理解,不过通过自己摸索再结合动画的形式来看,对理解算法的过程会更加明确。
结果是已经排好了,但是还不能让它们动起来,想要动起来,继续和我一步一步做下去。
想要让元素动起来前,我们需要有两个标识,一个左一个右。
左称为:.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其实哪有完美的,如果追求完美,需要优化的地方还有很多,比如:代码复用性、代码不简洁、命名是否规范、兼容性是否可行等等。
感兴趣的小伙伴可以自己去试试做下优化,我相信你们肯定比我强。
感谢您抽出宝贵的时间阅读本文,希望对您有所帮助。
如果您遇到什么疑问或者建议,欢迎多多交流,大家共同进步。
在阅读过程中,如果有不正确的地方,希望您能提出来,我会努力改正并提供更优质的文章。
*请认真填写需求信息,我们会在24小时内与您取得联系。