vlambda博客
学习文章列表

七夕节见男神,爱TA就送:JS版冒泡排序动画!


一眨眼的工夫,很快就又到了新一年的七夕节了,正好碰到今天公司搞了一个七夕小活动的工夫,就用JS写了一个冒泡排序算法,顺便写了动画排序过程。


其实这种算法动画效果网上有很多例子,但今天兴趣使然,就抽空想自己实现一下。


代码的优化这里就不做讨论了,完全是为了实现自己小小的满足感,感兴趣的童鞋可以自己在电脑上做一下代码优化。


下面是效果图(部分):


七夕节见男神,爱TA就送:JS版冒泡排序动画!







01


动态渲染移动元素



废话不多说,直接上代码(七夕节见男神,爱TA就送:JS版冒泡排序动画!大佬会做的比我更好):


// 渲染容器
<div id="container"></div>


// 要排序的数组
let arr = [27372501054112416424473112];
// 每个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);


htmlbody {
    margin0;
    padding0;
}
#container,
h1 {
    height100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
h1 {
    height: auto;
    margin6% 0;
}
.list {
    position: relative;
    display: flex;
    align-items: flex-end;
    list-style: none;
    padding0;
    margin0;
    margin-top10%;
}
.item {
    width45px;
    margin-right12px;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    background-colorrgb(173216230);
    font-size20px;
    color#333;
    pointer-events: none;
    transition: all .3s ease;
    position: absolute;
}
.item:last-child {
    margin-right0;
}
.item.left,
.item.right {
    background-colorrgb(01280);
}
.item.left::before,
.item.right::before {
    content"";
    position: absolute;
    left: -4px;
    top: -4px;
    widthcalc(100% + 4px);
    heightcalc(100% + 4px);
    border2px solid red;
}
.item.left::after,
.item.right::after {
    content"";
    position: absolute;
    left50%;
    bottom: -20px;
    width0;
    height0;
    border10px solid transparent;
    border-bottom-color: red;
    transformtranslateX(-50%);
}
.item.success {
    background-colorrgb(2551650);
}
.out {
    position: absolute;
    bottomcalc(100% + 2px);
    left50%;
    transformtranslateX(-50%);
    color#333;
}


在上面的代码中,
className = 'out'是用来判断数字是否已超出元素内部,如果超出,则显示在元素外部,而非内部。防止字体与元素显示交叉感。


七夕节见男神,爱TA就送:JS版冒泡排序动画!


${ item * 3 },乘以3完全是为了元素看起来更高大一些,要不然,当要排序的数值中包括<10的数字时,元素的高度就会几乎看不见。


七夕节见男神,爱TA就送:JS版冒泡排序动画!


${ posLeft * index },会随着元素的渲染,left值会随着index索引值的增大而改变。

  • 57 * 0 = 0
  • 57 * 1 = 57
  • 57 * 2 = 114
  • ...

${ posLeft * totalWidth + 48 },根据子元素渲染个数设置父元素的总宽度,因为子元素采用了定位布局,所以父元素撑不开,需要动态设置宽度,以适配页面的居中显示。

当然不设置父元素的宽度也不影响执行,只是会在一定程度上,列表会不方便居中在视窗的水平垂直居中位置。


七夕节见男神,爱TA就送:JS版冒泡排序动画!


此时已经渲染出列表了,但是没有执行排序算法,下面添加排序算法。







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);


七夕节见男神,爱TA就送:JS版冒泡排序动画!


冒泡排序算法有很多种方法,这里只介绍了一种,就是:套用两层循环,一个一个对比,如果找到符合的元素,就通过[arr[i], arr[j]] = [arr[j], arr[i]]数组解构的方式,调换两个元素的位置。


也许这样表达大家有些难以理解,当然高手飘过哈。其实我也当初不理解,不过通过自己摸索再结合动画的形式来看,对理解算法的过程会更加明确。


结果是已经排好了,但是还不能让它们动起来,想要动起来,继续和我一步一步做下去。






03


让元素动起来



想要让元素动起来前,我们需要有两个标识,一个左一个右。


左称为:.item.left 左边界元素。右称为:.item.right 右边界元素。我在这里用了这两个元素,完全是为了在执行动画的时候方便区分理解。


代表:左边界元素需要每次和右边界元素去对比,如果有小于左元素的,则进行调换,否则不动。


比如这样:


七夕节见男神,爱TA就送:JS版冒泡排序动画!


动的一直是右边界,而非左边界,左边界在这里只是充当一个基点的角色,用来和其它元素进行对比。


改造一下:


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]];
            }
        }
    }
}


此时刷新页面,好吧,我傻眼了。。。都乱了七夕节见男神,爱TA就送:JS版冒泡排序动画!


七夕节见男神,爱TA就送:JS版冒泡排序动画!



因为我在上面的CSS中加了300毫秒的元素动画时间transition: all .3s ease

但是循环太快了,还来不及做动画,元素在运动的过程中又再次执行下次渲染,就造成了这种无脑局面。。。

想要解决这个问题,需要有一种可以让循环慢下来的办法才行,下面我们可以这样做,想让它多慢,就有多慢。


七夕节见男神,爱TA就送:JS版冒泡排序动画!


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);
            }
        }
    }
}


我们可以使用asyncpromise语法搭配来模拟异步操作过程,在上面的例子中,循环的过程必须要等到sleep方法有返回值后,才可以进行后面的循环,采用传入的时间参数毫秒,来生成一个设置在时间范围内的定时器,直至等待返回。


七夕节见男神,爱TA就送:JS版冒泡排序动画!


这是怎么回事,虽然在有条理的做着动画,但是明显不对吧。都乱套了。。。七夕节见男神,爱TA就送:JS版冒泡排序动画!


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


最后



感谢您抽出宝贵的时间阅读本文,希望对您有所帮助。


如果您遇到什么疑问或者建议,欢迎多多交流,大家共同进步。


在阅读过程中,如果有不正确的地方,希望您能提出来,我会努力改正并提供更优质的文章。







结语

  • 关注后回复 资料免费领取学习资料
  • 关注后回复 进群拉你进技术交流群
  • 关注后回复 福利查看最新、即将开展的福利活动,先到先得
  • 欢迎关注 鲸选派,更多 「福利干货」及时推送