写在开头
哈喽,大家好!👋
最近工作真的好忙呀,进入996了,小编从业几年,第一次感受真正意义上的996,感觉人要寄了。😩
很久没去爬山了,以往每周末小编都会去爬山,几年里几乎把广州大大小小的山都爬了一遍了,挺喜欢登上高峰俯瞰山下的那一瞬间。😋
怼张雨后的山中图:

希望忙完这个四月,五月能空闲一些,再去勇攀高峰。👻
那么,回到正题,本次要分享的是 DOM 操作新面孔 ------ moveBefore ,虽然可能还不是那么普及 🤔,但也挺好用的,请诸君按需食用哈。

🧐 moveBefore 是个啥?
当下,在前端开发领域,单纯与 DOM 打交道的场景已十分罕见。随着技术的发展,在 Vue、React 等前端框架的助力下,数据驱动视图已成为主流开发模式。
但是,基础的 DOM 操作咱们还是需要懂滴,👉比如,如何移动一个 DOM 的位置?
通常咱们可能会用 appendChild
、 insertBefore
、 removeChild
这些老朋友来实现。
实际中,当涉及到在网页中移动元素时,DOM 传统上仅限于删除和插入。在过去的 20 年里,每当咱们作为开发人员在网页中 "移动" 元素时,幕后真正发生的事情是我们将该元素删除,然后插入到其他位置。
但有时候,只是想把一个节点移动到同一个父节点下的另一个节点前面,有没有更 "直观" 一点的方法呢?
噔噔噔噔!✨ moveBefore
闪亮登场!
简单来说, moveBefore
是 Node 接口 上的一个方法,它的作用是将当前父节点下的一个子节点移动到另一个指定的子节点之前 。
它的语法瞅着是这样子的👉:
javascript
// 指定的子节点之前
parentNode.moveBefore(nodeToMove, referenceNode);
- parentNode : 你要移动的节点所在的父节点。
- nodeToMove : 你想要移动的那个节点,它必须是 parentNode 的一个子节点。
- referenceNode : 参考节点,nodeToMove 会被移动到这个节点的前面。它也必须是 parentNode 的一个子节点。 特殊情况 :如果 referenceNode 是 null 或者省略了,那么 nodeToMove 会被移动到 parentNode 子节点列表的末尾 ,效果类似于
appendChild
。
javascript
// 移动到末尾
parentNode.moveBefore(nodeToMove, null);
听起来是不是比先 removeChild
再 insertBefore
要简洁一些?(o゚v゚)ノ
🚀 基础示例:移动段落
咱们来看个简单的例子,假设我们有下面这样的 HTML:
html
<div id="container">
<p id="p1">我是段落1</p>
<p id="p2">我是段落2</p>
<p id="p3">我是段落3</p>
</div>
<button id="moveBtn">把 P3 移动到 P2 前面</button>
现在,我们想把 p3 移动到 p2 的前面,用 moveBefore
就可以这样写:
javascript
const container = document.getElementById('container');
const p3 = document.getElementById('p3');
const p2 = document.getElementById('p2');
const moveBtn = document.getElementById('moveBtn');
moveBtn.addEventListener('click', () => {
if (container && p3 && p2) {
console.log('开始移动 P3 到 P2 前面...');
container.moveBefore(p3, p2); // 就是这一行!
console.log('移动完成!🎉');
} else {
console.error('找不到必要的元素 Σ( ° △ °|||)');
}
});

点击按钮后,DOM 结构就会变成:
html
<div id="container">
<p id="p1">我是段落1</p>
<p id="p3">我是段落3</p>
<p id="p2">我是段落2</p>
</div>
是不是很简单直观?(ノ´ヮ´)ノ*:・゚✧
🚀 进阶示例:移动排序
moveBefore
的核心在于在同一个父节点内重新排序子节点,基于这个特性,咱们可以想到一些有趣的应用场景,如:通过点击 "上移" 和 "下移" 按钮来调整可排序的待办事项列表。
HTML 结构:
html
<ul id="todo-list">
<li>
<span>任务 A</span>
<button class="up-btn">🔼</button>
<button class="down-btn">🔽</button>
</li>
<li>
<span>任务 B</span>
<button class="up-btn">🔼</button>
<button class="down-btn">🔽</button>
</li>
<li>
<span>任务 C</span>
<button class="up-btn">🔼</button>
<button class="down-btn">🔽</button>
</li>
</ul>
CSS 样式:
css
#todo-list li {
margin-bottom: 5px;
padding: 5px;
background-color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
#todo-list button {
margin-left: 5px;
cursor: pointer;
}
JavaScript 逻辑:
javascript
const todoList = document.getElementById('todo-list');
todoList.addEventListener('click', (event) => {
const target = event.target;
const currentLi = target.closest('li'); // 找到当前点击按钮所在的 li
if (!currentLi || !todoList.contains(currentLi)) return; // 防御性检查
if (target.classList.contains('up-btn')) {
// 上移按钮逻辑
const prevLi = currentLi.previousElementSibling; // 找到前一个兄弟 li
if (prevLi) {
// 使用 moveBefore 将当前 li 移动到前一个 li 的前面
// 注意:moveBefore 是父节点的方法
todoList.moveBefore(currentLi, prevLi);
console.log('上移成功!🚀');
} else {
console.log('已经是第一个啦,不能再上移了!');
}
} else if (target.classList.contains('down-btn')) {
// 下移按钮逻辑
const nextLi = currentLi.nextElementSibling; // 找到后一个兄弟 li
if (nextLi) {
// 要移动到 nextLi 的后面,相当于移动到 nextLi 的下一个兄弟节点的前面
// 如果 nextLi 是最后一个,referenceNode 就是 null
todoList.moveBefore(currentLi, nextLi.nextElementSibling);
console.log('下移成功!🚀');
} else {
console.log('已经是最后一个啦,不能再下移了!');
}
}
});
现在,点击任务旁边的上下箭头,就可以调整它们的顺序啦! 🎉🎉🎉
效果:

🚀 高阶示例:拖动排序
上面,点击排序虽然能用,但是存在一些局限性,排序功能咱们可能更多的是希望能进行拖动交互,这样用户体验度更良好一些。
虽然完整的拖动排序实现比较复杂,但其核心的 DOM 操作也可以用 moveBefore
来简化。
打个广,小编写过非常多的拖动案例文章,欢迎查阅:传送门。
HTML 结构:
html
<div class="kanban-column" id="todo-column">
<h3>待办 (To Do)</h3>
<div class="task-card" id="task1" draggable="true">任务1</div>
<div class="task-card" id="task2" draggable="true">任务2</div>
<div class="task-card" id="task3" draggable="true">任务3</div>
<div class="task-card" id="task4" draggable="true">任务4</div>
</div>
CSS 样式:
css
.kanban-column {
border: 1px solid #ccc;
padding: 10px;
min-height: 100px;
background-color: #f9f9f9;
margin-bottom: 10px;
}
.task-card {
border: 1px solid #ddd;
background-color: white;
padding: 8px;
margin-bottom: 5px;
cursor: grab;
}
.task-card.dragging {
opacity: 0.5;
border: 2px dashed #000;
}
.drag-over {
background-color: #e0e0e0;
}
Javascript 逻辑:
js
const column = document.getElementById("todo-column");
const tasks = column.querySelectorAll(".task-card");
let draggedTask = null; // 用于存储当前拖动的元素
// 1. 为所有任务卡片添加 dragstart 和 dragend 事件
tasks.forEach((task) => {
task.addEventListener("dragstart", (event) => {
draggedTask = task; // 记录被拖动的元素
event.dataTransfer.setData("text/plain", task.id); // 存储ID,虽然此例中直接用变量更方便
setTimeout(() => task.classList.add("dragging"), 0); // 延迟添加样式,避免影响拖拽图像
console.log(`开始拖动: ${task.id}`);
});
task.addEventListener("dragend", () => {
if (draggedTask) {
draggedTask.classList.remove("dragging"); // 移除拖动样式
console.log(`结束拖动: ${draggedTask.id}`);
draggedTask = null; // 清理
}
});
});
// 2. 为列(放置区域)添加 dragover 和 drop 事件
column.addEventListener("dragover", (event) => {
event.preventDefault(); // 必须阻止默认行为才能触发 drop
const afterElement = getDragAfterElement(column, event.clientY); // 获取应该插入到哪个元素之前
});
column.addEventListener("drop", (event) => {
event.preventDefault(); // 阻止默认行为(比如打开链接)
if (!draggedTask) return; // 如果没有拖动中的任务,则不处理
// 使用 moveBefore 进行移动
// 如果 afterElement 为 null,moveBefore 会将元素移动到末尾
column.moveBefore(draggedTask, afterElement);
// dragend 事件会处理样式的移除
});
// 辅助函数:根据 Y 坐标找到应该插入到哪个元素之前
function getDragAfterElement(container, y) {
const draggableElements = [
...container.querySelectorAll(".task-card:not(.dragging)"),
];
return draggableElements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2; // 计算鼠标位置与元素中点的垂直距离
// 如果 offset 小于 0(鼠标在元素上半部分)且比当前记录的 closest 更近
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY }
).element; // 初始 closest 的 offset 设为负无穷
}
效果:

虽然没有很好的过渡效果,但是功能确是实打实的搞定了。当然,由于 moveBefore
只能在同一个父节点内移动元素,如果你想实现分组之间的拖动效果, moveBefore
就无能为力了,你还是需要使用 removeChild
和 appendChild
或 insertBefore
等的组合,这一点要记住哦!😋
🎉 总结
moveBefore
API 提供了一种更直观的方式来在同一个父节点内移动子节点到指定位置之前。它的语法简洁,意图明确。
然而,由于它并非广泛支持的标准 API,目前更像是一个有趣的提案或实验性功能。在实际开发中,小编仍然推荐使用稳定且兼容性良好的 removeChild
和 appendChild
或 insertBefore
等的组合,就是麻烦一点。😑
希望这次的探索之旅让你对 DOM 操作有了新的认识!如果你有任何想法或者发现了 moveBefore
的最新动态,欢迎在评论区分享!下次再见!👋👋👋
至此,本篇文章就写完啦,撒花撒花。
