DOM 移动大法!✨ moveBefore API 探秘 & 实战 (≧∇≦)/

写在开头

哈喽,大家好!👋

最近工作真的好忙呀,进入996了,小编从业几年,第一次感受真正意义上的996,感觉人要寄了。😩

很久没去爬山了,以往每周末小编都会去爬山,几年里几乎把广州大大小小的山都爬了一遍了,挺喜欢登上高峰俯瞰山下的那一瞬间。😋

怼张雨后的山中图:

希望忙完这个四月,五月能空闲一些,再去勇攀高峰。👻

那么,回到正题,本次要分享的是 DOM 操作新面孔 ------ moveBefore ,虽然可能还不是那么普及 🤔,但也挺好用的,请诸君按需食用哈。

🧐 moveBefore 是个啥?

当下,在前端开发领域,单纯与 DOM 打交道的场景已十分罕见。随着技术的发展,在 Vue、React 等前端框架的助力下,数据驱动视图已成为主流开发模式。

但是,基础的 DOM 操作咱们还是需要懂滴,👉比如,如何移动一个 DOM 的位置?

通常咱们可能会用 appendChildinsertBeforeremoveChild 这些老朋友来实现。

实际中,当涉及到在网页中移动元素时,DOM 传统上仅限于删除和插入。在过去的 20 年里,每当咱们作为开发人员在网页中 "移动" 元素时,幕后真正发生的事情是我们将该元素删除,然后插入到其他位置。

但有时候,只是想把一个节点移动到同一个父节点下的另一个节点前面,有没有更 "直观" 一点的方法呢?

噔噔噔噔!✨ moveBefore 闪亮登场!

简单来说, moveBeforeNode 接口 上的一个方法,它的作用是将当前父节点下的一个子节点移动到另一个指定的子节点之前

它的语法瞅着是这样子的👉:

javascript 复制代码
// 指定的子节点之前 
parentNode.moveBefore(nodeToMove, referenceNode);
  • parentNode : 你要移动的节点所在的父节点。
  • nodeToMove : 你想要移动的那个节点,它必须是 parentNode 的一个子节点。
  • referenceNode : 参考节点,nodeToMove 会被移动到这个节点的前面。它也必须是 parentNode 的一个子节点。 特殊情况 :如果 referenceNode 是 null 或者省略了,那么 nodeToMove 会被移动到 parentNode 子节点列表的末尾 ,效果类似于 appendChild
javascript 复制代码
// 移动到末尾
parentNode.moveBefore(nodeToMove, null);

听起来是不是比先 removeChildinsertBefore 要简洁一些?(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 就无能为力了,你还是需要使用 removeChildappendChildinsertBefore 等的组合,这一点要记住哦!😋

🎉 总结

moveBefore API 提供了一种更直观的方式来在同一个父节点内移动子节点到指定位置之前。它的语法简洁,意图明确。

然而,由于它并非广泛支持的标准 API,目前更像是一个有趣的提案或实验性功能。在实际开发中,小编仍然推荐使用稳定且兼容性良好的 removeChildappendChildinsertBefore 等的组合,就是麻烦一点。😑

希望这次的探索之旅让你对 DOM 操作有了新的认识!如果你有任何想法或者发现了 moveBefore 的最新动态,欢迎在评论区分享!下次再见!👋👋👋


至此,本篇文章就写完啦,撒花撒花。

相关推荐
fakaifa9 分钟前
【最新版】沃德代驾源码全开源+前端uniapp
前端·小程序·uni-app·开源·php·沃德代驾·代驾小程序
泯泷15 分钟前
【SHA-2系列】SHA256 前端安全算法 技术实践
javascript·安全·node.js
清羽_ls23 分钟前
leetcode-位运算
前端·算法·leetcode·位运算
李菠菜39 分钟前
利用Nginx实现高性能的前端打点采集服务(支持GET和POST)
linux·前端·nginx
lilye6641 分钟前
精益数据分析(6/126):深入理解精益分析的核心要点
前端·人工智能·数据分析
Apifox1 小时前
Apifox 4月更新|Apifox在线文档支持LLMs.txt、评论支持使用@提及成员、支持为团队配置「IP 允许访问名单」
前端·后端·ai编程
Jolyne_1 小时前
搭建公司前端脚手架
前端·架构·前端框架
hang_bro1 小时前
el-tree的动态加载问题与解决
前端·javascript·vue.js
天天扭码1 小时前
一杯珍珠奶茶的时间吃透一道高频面试算法题——搜索二位矩阵Ⅱ
前端·算法·面试
il1 小时前
Deepdive into Tanstack Query - 1.0
前端