前端列表排序的多种实现方案与性能对比实战
先看效果,再看代码
上周改一个后台管理页的排序功能,用户要求能拖拽调整列表顺序。我一开始想用现成的库,比如 SortableJS,但项目里已经有一堆依赖了,不想再加。于是决定自己撸一个轻量版。折腾了半天,发现核心逻辑其实就几十行代码,关键在于怎么处理 DOM 移动和数据同步。
下面这个方案亲测有效,支持鼠标和触屏(简单场景),而且不依赖任何第三方库。直接上核心代码:
<ul id="sortable-list">
<li data-id="1">Item 1</li>
<li data-id="2">Item 2</li>
<li data-id="3">Item 3</li>
<li data-id="4">Item 4</li>
</ul>
#sortable-list {
list-style: none;
padding: 0;
margin: 0;
}
#sortable-list li {
padding: 12px;
background: #f5f5f5;
border: 1px solid #ddd;
margin-bottom: 4px;
cursor: move;
user-select: none;
}
#sortable-list li.dragging {
opacity: 0.6;
background: #e0e0e0;
}
const list = document.getElementById('sortable-list');
let draggedItem = null;
list.addEventListener('dragstart', e => {
draggedItem = e.target;
setTimeout(() => e.target.classList.add('dragging'), 0);
});
list.addEventListener('dragend', e => {
e.target.classList.remove('dragging');
// 同步数据逻辑放这里
updateOrder();
});
list.addEventListener('dragover', e => {
e.preventDefault();
});
list.addEventListener('drop', e => {
e.preventDefault();
if (draggedItem !== e.target) {
list.insertBefore(draggedItem, e.target);
}
});
function updateOrder() {
const order = Array.from(list.children).map(li => li.dataset.id);
console.log('新顺序:', order);
// 这里可以发请求:fetch('https://jztheme.com/api/update-order', { method: 'POST', body: JSON.stringify(order) })
}
踩坑提醒:这三点一定注意
写完第一版后,测试时发现一堆问题。这里重点说三个我踩过好几次的坑:
- dragover 必须 preventDefault:不然 drop 事件根本不会触发。这是 HTML5 Drag API 的反人类设计之一,文档里写了但很容易忽略。
- 移动端基本不可用:HTML5 拖拽在 iOS Safari 上支持极差,安卓也各种不一致。如果你要做跨端,建议直接上 touch 事件手写,或者老老实实用库。我后来在另一个项目里用 touchmove 实现了一套,但代码量翻倍,而且要处理滚动冲突。
- 动态插入元素要重新绑定:如果列表是通过 JS 动态生成的(比如分页加载),记得重新给新元素加上 draggable=”true” 属性,否则拖不动。我有次上线后才发现新加载的数据不能拖,尴尬死了。
另外,dragstart 里加 setTimeout 是为了确保样式类能正确应用——因为浏览器在 dragstart 触发时可能还没完全进入拖拽状态,直接加类有时会失效。这个 trick 是我在 Stack Overflow 上扒到的,实测有效。
这个场景最好用:带动画的平滑过渡
产品经理总喜欢加“丝滑动画”。原生拖拽移动 DOM 节点是瞬移的,看起来很生硬。我试过几种方案,最简单的办法是在 CSS 里加个 transition:
#sortable-list li {
transition: transform 0.2s ease, opacity 0.2s ease;
}
但问题来了:拖拽过程中频繁触发重排,动画会卡顿甚至错乱。后来我改用 transform 模拟位置变化,但实现起来太复杂,最后妥协了——只在 drop 完成后加一个短暂的高亮反馈,比如背景色闪一下:
function highlightItem(item) {
item.style.backgroundColor = '#ffeb3b';
setTimeout(() => {
item.style.backgroundColor = '';
}, 300);
}
// 在 drop 事件里调用
list.addEventListener('drop', e => {
// ... 插入逻辑
highlightItem(draggedItem);
});
虽然不是真正的位移动画,但用户感知上“有反馈”就够了。毕竟我们不是做交互动效比赛,能用就行。
进阶技巧:和 Vue/React 数据联动
如果你在用框架,千万别直接操作 DOM。我见过有人在 React 里混用原生拖拽和 setState,结果状态和 UI 对不上,debug 到凌晨三点。
正确姿势是:把拖拽逻辑封装成指令或 Hook,只负责计算新顺序索引,然后通知上层更新状态。比如在 Vue 3 里可以这样写:
// composables/useSortable.js
export function useSortable(listRef, onUpdate) {
let draggedIndex = null;
const onDragStart = (e, index) => {
draggedIndex = index;
e.target.classList.add('dragging');
};
const onDrop = (e, targetIndex) => {
if (draggedIndex === null || draggedIndex === targetIndex) return;
onUpdate(draggedIndex, targetIndex); // 通知父组件交换位置
e.target.classList.remove('dragging');
};
return { onDragStart, onDrop };
}
然后在组件里:
<template>
<li
v-for="(item, index) in items"
:key="item.id"
draggable="true"
@dragstart="onDragStart($event, index)"
@dragover.prevent
@drop="onDrop($event, index)"
>
{{ item.name }}
</li>
</template>
<script setup>
import { ref } from 'vue';
import { useSortable } from './composables/useSortable';
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
// ...
]);
const { onDragStart, onDrop } = useSortable(null, (from, to) => {
const moved = items.value.splice(from, 1)[0];
items.value.splice(to, 0, moved);
});
</script>
这样数据流清晰,也不会和框架的渲染机制打架。React 的思路类似,用 useCallback 包裹事件处理器就行。
别忘了防抖和错误处理
线上环境一定要考虑网络失败的情况。比如用户拖完后发请求,结果 500 了,这时候 UI 已经变了,但数据没保存。我的做法是:
- 先 revert UI 到原始状态
- 弹个 toast 提示“保存失败,请重试”
- 提供“重试”按钮,用缓存的 order 再发一次
另外,高频拖拽可能导致多次请求。虽然用户不太可能一秒拖十次,但加个 debounce 更稳妥:
let saveTimeout;
function updateOrder() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
const order = /* 获取顺序 */;
fetch('https://jztheme.com/api/update-order', {
method: 'POST',
body: JSON.stringify(order)
}).catch(err => {
console.error('保存失败', err);
// revert UI + 提示
});
}, 500);
}
结尾碎碎念
列表排序看着简单,真做起来细节一堆。我这个方案适合中小项目快速落地,如果要做复杂的嵌套排序、跨容器拖拽,还是上 dnd-kit 或 SortableJS 吧,省心。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多(比如结合虚拟滚动、树形结构排序),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流——特别是移动端的优雅解法,求推荐!

暂无评论