拖拽排序时列表项位置错乱怎么办?

司马舒昕 阅读 25

我用原生 JS 实现了一个简单的拖拽排序功能,但松开鼠标后列表项的位置总是不对,有时候还会重复或者消失。明明拖动时视觉反馈是正常的,可一 drop 就乱了。

下面是我目前的 HTML 结构,每个 li 都加了 draggable=”true”,也监听了 dragstart、dragover 和 drop 事件,但数据更新和 DOM 顺序好像没对齐:

<ul id="sortable-list">
  <li draggable="true">Item 1</li>
  <li draggable="true">Item 2</li>
  <li draggable="true">Item 3</li>
  <li draggable="true">Item 4</li>
</ul>
我来解答 赞 1 收藏
二维码
手机扫码查看
1 条解答
心霞 Dev
这个问题太经典了,通常是因为试图在 drop 事件里才去处理 DOM 交换,导致索引计算和实际状态对不上。按照 HTML5 拖拽规范的建议,应该在 dragover 事件中实时计算位置并移动 DOM,而不是等到松手。dragover 事件在鼠标移动时会高频触发,正好用来做实时的位置预判和插入。

核心思路是:获取鼠标当前 Y 坐标,计算它位于列表中哪个元素的上方或下方,然后利用 insertBefore 实时调整 DOM 结构。这样当你松开鼠标(触发 drop)时,DOM 顺序已经是正确的了,数据源只需要在 dragend 时根据 DOM 重新获取即可。

下面是标准写法的完整代码,直接就能用:

const list = document.getElementById('sortable-list');

let draggedItem = null;

// 1. 开始拖拽
list.addEventListener('dragstart', (e) => {
draggedItem = e.target;
// 延时添加样式,让拖拽时的“幽灵图”保持原样,原身变半透明
setTimeout(() => {
draggedItem.classList.add('dragging');
}, 0);
});

// 2. 拖拽结束
list.addEventListener('dragend', (e) => {
e.target.classList.remove('dragging');
draggedItem = null;
// 这里是同步数据的最佳时机
// const newOrder = [...list.children].map(li => li.textContent);
// console.log('更新后的数据顺序:', newOrder);
});

// 3. 拖拽经过容器(核心逻辑)
list.addEventListener('dragover', (e) => {
e.preventDefault(); // 必须阻止默认行为才能允许 drop

const afterElement = getDragAfterElement(list, e.clientY);
const draggable = document.querySelector('.dragging');

if (!draggable) return;

if (afterElement == null) {
// 如果后面没有元素了,就追加到最后
list.appendChild(draggable);
} else {
// 否则就插入到该元素之前
list.insertBefore(draggable, afterElement);
}
});

// 辅助函数:计算鼠标下方最接近的元素
function getDragAfterElement(container, y) {
// 获取除了正在拖拽的元素之外的所有 li
const draggableElements = [...container.querySelectorAll('li:not(.dragging)')];

return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
// 计算鼠标 Y 坐标与元素中心的偏移量
const offset = y - box.top - box.height / 2;

// offset < 0 意味着鼠标在元素上方
// 我们要找的是 offset 为负数且最接近 0 的那个元素
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}


另外,CSS 里记得加个样式给拖拽中的元素,比如 .dragging { opacity: 0.5; },不然视觉上可能看不出哪个在动。

这种做法的好处是 DOM 操作和视觉反馈是同步的,不用去维护复杂的索引数组,最后直接读 DOM 就是最新的顺序,省心又不容易出 Bug。
点赞
2026-03-03 20:07