深入理解前端拖拽:从基础实现到事件冒泡与委托的应用【面试真题】

news2024/9/21 23:57:53

在前端开发中,拖拽功能是一项常见的交互需求。通过监听鼠标或触摸事件,用户可以拖动元素并将其放置到指定位置。理解拖拽的底层实现、如何判断拖拽的是子元素还是父元素,以及事件冒泡事件委托的原理,可以帮助我们更好地实现复杂的拖拽交互。

一、拖拽的底层实现

1.1 拖拽的核心步骤

实现拖拽功能主要依赖以下几个核心事件:

  • mousedown:监听鼠标按下事件,标志着拖拽开始。
  • mousemove:当鼠标移动时,跟踪鼠标位置,更新元素的位置。
  • mouseup:当鼠标释放时,结束拖拽操作。

1.2 基本拖拽实现示例

以下是一个简单的拖拽实现过程:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Drag and Drop</title>
  <style>
    #draggable {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
      cursor: pointer;
    }
  </style>
</head>
<body>

  <div id="draggable"></div>

  <script>
    const draggable = document.getElementById('draggable');

    let isDragging = false;
    let offsetX, offsetY;

    // 鼠标按下事件,开始拖拽
    draggable.addEventListener('mousedown', (e) => {
      isDragging = true;
      // 记录点击位置相对于元素的偏移
      offsetX = e.clientX - draggable.offsetLeft;
      offsetY = e.clientY - draggable.offsetTop;
    });

    // 鼠标移动事件,更新元素位置
    document.addEventListener('mousemove', (e) => {
      if (isDragging) {
        draggable.style.left = `${e.clientX - offsetX}px`;
        draggable.style.top = `${e.clientY - offsetY}px`;
      }
    });

    // 鼠标释放事件,结束拖拽
    document.addEventListener('mouseup', () => {
      isDragging = false;
    });
  </script>

</body>
</html>

实现效果
在这个示例中,红色方块能够随着鼠标的移动而拖动,当用户按下鼠标并移动时,方块的位置会实时更新。当用户松开鼠标时,方块停止拖动。这是实现基础拖拽功能的最简化实现。

请添加图片描述

1.3 拖拽的触摸支持

为了在移动设备上支持拖拽,可以监听 touchstarttouchmovetouchend 事件。实现方式与鼠标事件类似。

draggable.addEventListener('touchstart', (e) => {
  isDragging = true;
  const touch = e.touches[0];
  offsetX = touch.clientX - draggable.offsetLeft;
  offsetY = touch.clientY - draggable.offsetTop;
});

document.addEventListener('touchmove', (e) => {
  if (isDragging) {
    const touch = e.touches[0];
    draggable.style.left = `${touch.clientX - offsetX}px`;
    draggable.style.top = `${touch.clientY - offsetY}px`;
  }
});

document.addEventListener('touchend', () => {
  isDragging = false;
});

实现效果
这个示例扩展了拖拽功能,使其在移动设备上也能正常工作。拖拽时,用户可以使用手指在触摸屏上拖动红色方块,松开手指后方块会停止移动。此功能在移动设备中尤为重要,因为鼠标事件不会在触屏设备上触发。

请添加图片描述

1.4 边界处理与限制

可以根据父元素的尺寸或窗口大小,限制可拖拽区域,避免元素被拖出可视范围。

document.addEventListener('mousemove', (e) => {
  if (isDragging) {
    const newX = e.clientX - offsetX;
    const newY = e.clientY - offsetY;

    // 限制元素位置在窗口范围内
    draggable.style.left = `${Math.max(0, Math.min(window.innerWidth - draggable.offsetWidth, newX))}px`;
    draggable.style.top = `${Math.max(0, Math.min(window.innerHeight - draggable.offsetHeight, newY))}px`;
  }
});

实现效果
在这个示例中,拖拽的元素会被限制在浏览器窗口的范围内,无法被拖出屏幕边界。这样可以避免元素被拖出视线之外,从而改善用户体验。

请添加图片描述

二、判断拖拽的是子元素还是父元素

2.1 事件目标和父元素关系

在拖拽过程中,可以通过事件目标event.target)和元素的 DOM 层级关系来判断拖拽的是哪个元素。

document.addEventListener('mousedown', (e) => {
  const target = e.target;
  if (target.classList.contains('child')) {
    console.log('拖拽的是子元素');
  } else if (target.classList.contains('parent')) {
    console.log('拖拽的是父元素');
  }
});
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Drag Parent or Child</title>
  <style>
    .parent {
      width: 200px;
      height: 200px;
      background-color: lightblue;
      position: relative;
    }

    .child {
      width: 100px;
      height: 100px;
      background-color: coral;
      position: absolute;
      cursor: pointer;
    }
  </style>
</head>
<body>

  <div id="parent" class="parent">
    <div id="child" class="child"></div>
  </div>

  <script>
    const parent = document.getElementById('parent');
    const child = document.getElementById('child');

    parent.addEventListener('mousedown', (e) => {
      if (e.target === child) {
        console.log('拖拽的是子元素');
      } else if (e.target === parent) {
        console.log('拖拽的是父元素');
      }
    });
  </script>

</body>
</html>

在上述代码中,我们通过 classList.contains() 判断点击的元素是父元素还是子元素。

实现效果
在这个示例中,当用户点击父元素或子元素时,会在控制台中输出不同的信息,指示用户当前正在拖拽的是哪个元素。这对于管理嵌套元素的拖拽行为非常有帮助。

请添加图片描述

2.2 contains() 方法

如果需要判断某个元素是否包含另一个元素,可以使用 DOM API 中的 contains() 方法:

const parent = document.getElementById('parent');
const child = document.getElementById('child');

if (parent.contains(child)) {
  console.log('子元素是父元素的子节点');
}

这在复杂嵌套关系下,帮助判断拖拽的是哪个具体元素。

三、事件冒泡和事件委托

3.1 事件冒泡

事件冒泡是指事件从触发目标元素开始,逐层向其父元素传播的过程。在 JavaScript 中,默认情况下,事件会从最具体的元素(目标元素)开始,然后逐级向上传播,直到 document 或者根元素。

例如,点击子元素时,事件会依次传递到父元素,祖父元素,直到根元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Event Bubbling</title>
  <style>
    #parent {
      width: 200px;
      height: 200px;
      background-color: lightblue;
      position: relative;
    }

    #child {
      width: 100px;
      height: 100px;
      background-color: coral;
      position: absolute;
      cursor: pointer;
    }
  </style>
</head>
<body>

  <div id="parent">
    <div id="child">子元素</div>
  </div>

  <script>
    document.getElementById('parent').addEventListener('click', () => {
      console.log('父元素被点击');
    });

    document.getElementById('child').addEventListener('click', () => {
      console.log('子元素被点击');
    });
  </script>

</body>
</html>

在这个例子中,如果点击子元素,浏览器会先执行子元素的点击事件处理函数,然后再执行父元素的点击事件处理函数,这就是事件冒泡。

实现效果
当点击子元素时,事件会首先在子元素上触发,然后继续冒泡至父元素,最终触发父元素的点击事件。这说明了事件冒泡机制如何在 DOM 树中逐层传播。

请添加图片描述

3.2 事件委托

事件委托是利用事件冒泡机制,将子元素的事件监听器绑定到其父元素上,而不是直接绑定在每个子元素上。这样可以减少监听器的数量,特别是在动态生成子元素的场景下十分有效。

<ul id="list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<script>
  document.getElementById('list').addEventListener('click', (e) => {
    if (e.target.tagName === 'LI') {
      console.log('点击了:', e.target.textContent);
    }
  });
</script>

在上述代码中,虽然我们没有给每个 li 元素添加事件监听器,但通过给 ul 父元素绑定一个监听器,借助事件冒泡机制,我们可以捕获 li 的点击事件。

实现效果
在这个例子中,我们只需要在父元素(ul)上绑定一个点击事件监听器,就可以捕获所有子元素(li)的点击事件。这种方式减少了内存消耗,并且简化了事件管理,特别是在需要动态添加或删除子元素时。

请添加图片描述

3.3 使用事件委托的好处

  • 减少内存消耗:相比于为每个子元素单独绑定监听器,事件委托只需要给父元素绑定一个监听器,减少了监听器的数量。
  • 动态元素支持:事件委托可以很好地处理动态生成的子元素,无需手动为新元素绑定监听器。

3.4 stopPropagation() 阻止冒泡

在某些情况下,我们希望阻止事件冒泡,可以使用 event.stopPropagation() 方法。

document.getElementById('child').addEventListener('click', (e) => {
  e.stopPropagation();  // 阻止事件冒泡
  console.log('只触发子元素的点击事件');
});

在这里,点击子元素时,父元素的点击事件将不会被触发,因为冒泡过程被阻止了。

实现效果
当你点击子元素时,事件将只在子元素上触发,而不会冒泡到父元素。这对于控制特定的事件行为非常有用,特别是在你不希望某些事件影响其他层级的元素时。

请添加图片描述

四、拖拽与事件委托的结合

在复杂的拖拽场景中,可以结合事件委托简化事件监听器的管理。例如,给多个可拖拽的子元素进行拖拽处理时,可以将监听器绑定到父元素,并通过事件冒泡判断拖拽的具体子元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Drag with Delegation</title>
  <style>
    #container {
      width: 400px;
      height: 400px;
      background-color: lightgray;
      position: relative;
    }

    .draggable {
      width: 100px;
      height: 100px;
      background-color: coral;
      position: absolute;
      cursor: pointer;
    }
  </style>
</head>
<body>

  <div id="container">
    <div class="draggable" style="top: 50px; left: 50px;"></div>
    <div class="draggable" style="top: 150px; left: 150px;"></div>
  </div>

  <script>
    const container = document.getElementById('container');
    let draggedElement = null;
    let offsetX, offsetY;

    container.addEventListener('mousedown', (e) => {
      if (e.target.classList.contains('draggable')) {
        draggedElement = e.target;
        offsetX = e.clientX - draggedElement.offsetLeft;
        offsetY = e.clientY - draggedElement.offsetTop;
      }
    });

    container.addEventListener('mousemove', (e) => {
      if (draggedElement) {
        draggedElement.style.left = `${e.clientX - offsetX}px`;
        draggedElement.style.top = `${e.clientY - offsetY}px`;
      }
    });

    container.addEventListener('mouseup', () => {
      draggedElement = null;
    });
  </script>

</body>
</html>

通过这种方式,可以简化多个元素的拖拽处理逻辑,并且可以动态支持新生成的可拖拽元素。

实现效果
在这个示例中,用户可以拖动多个子元素,且无需为每个元素单独添加监听器。事件委托的使用使得父容器可以统一管理所有子元素的拖拽操作,代码更加简洁和高效。

请添加图片描述


总结:

  1. 拖拽的实现依赖于鼠标或触摸事件的监听,关键事件包括 mousedownmousemovemouseup
  2. 判断拖拽的是子元素还是父元素可以通过 event.target 和 DOM 层级关系实现,使用 contains() 方法可以判断元素的父子关系。
  3. 事件冒泡是事件从目标元素逐级向父元素传播的机制,可以通过 stopPropagation() 阻止冒泡。
  4. 事件委托可以通过将事件绑定在父元素上,利用冒泡机制处理子元素事件,有助于简化监听器管理,特别适合动态生成的子元素。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2153649.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

stm32 FLASH闪存(读写内部FLASH读取芯片ID)

理论 1.FLASH简介 STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分&#xff0c;通过闪存存储器接口&#xff08;外设&#xff09;(FLASH管理员)可以对程序存储器和选项字节进行擦除和编程 读写FLASH的用途&#xff1a; 利用程序存储器的剩余空间来保存掉电不…

C++—vector的常见接口与用法(正式进入STL)

目录 0.提醒 1.介绍 2.构造 1.正常构造 2.默认值构造 3.调用默认构造函数构造 3.遍历 1.迭代器 2.范围for 3.下标访问 4.容量 1.capacity&#xff1a;返回当前容器的容量 2.reserve&#xff1a;如果传的k比当前容量大&#xff0c;则扩容到比k大或者等于k的数&…

为人机交互保持预见性丨基于G32A1445的T-BOX应用方案

T-BOX是一种集成了通信、计算和控制功能的车载信息处理终端&#xff0c;通过车辆与云端、移动网络等进行数据交互&#xff0c;用于车、人、外部环境的互联互通&#xff0c;支持车辆定位、车载通信、远程控制、故障诊断、数据传输、紧急呼叫等功能&#xff0c;帮助车辆实现更加智…

2023北华大学程序设计新生赛部分题解

时光如流水般逝去&#xff0c;我已在校园中奋战大二&#xff01;(≧▽≦) 今天&#xff0c;静静回顾去年的新生赛&#xff0c;心中涌起无尽感慨&#xff0c;仿佛那段青春岁月如烟花般绚烂。✧&#xff61;(≧▽≦)&#xff61;✧ 青春就像一场燃烧的盛宴&#xff0c;激情澎湃&…

什么情况下会导致索引失效?

什么情况下会导致索引失效&#xff1f; 1. 组合索引非最左前缀2. LIKE查询%开头3. 字符串未加引号4. 不等比较5. 索引列运算6. OR连接查询 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1. 组合索引非最左前缀 描述&#xff1a;在组合索引…

基于springboot的驾校管理系统的设计与实现 (含源码+sql+视频导入教程)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于springboot的驾校管理系统4拥有三种角色&#xff0c;分别为管理员、教练、学员。 管理员&#xff1a;学员/教练管理、报名管理、车辆管理、教练分配管理、课程安排管理、考勤打卡管理…

【HTTP】构造HTTP请求和状态码

状态码 用于响应中&#xff0c;表示响应的结果如何 正确&#xff1f;错误&#xff1f;什么原因&#xff1f; HTTP 中的状态码都是标准约定好的 200 OK 成功了&#xff0c;一切顺利 在抓包到的响应中 404 Not Found 访问的资源&#xff08;URL 中的路径&#xff09;没找…

想要让ai做ppt?试试这四个!

今天咱们来聊点新鲜的&#xff0c;就是那些能让我们从繁琐的PPT制作中解脱出来的智能工具。你是否还在为制作PPT熬夜到天亮&#xff1f;别担心&#xff0c;我这就带你看看目前市面上最火的几款智能PPT生成工具&#xff0c;它们的表现如何呢&#xff1f;让我们一探究竟&#xff…

BUUCTF逆向wp [WUSTCTF2020]Cr0ssfun

第一步 查壳&#xff0c;本题是64位&#xff0c;无壳。 第二步 查看主函数&#xff0c;点开看主函数&#xff0c;没什么东西。 左边表里面看到好几个i开头的函数&#xff08;红色方框里面&#xff09;&#xff0c;点开看后每个函数的最后末尾&#xff08;图中红色椭圆圈那里&a…

MyBatis-config.xml核心配置

MyBatis-config.xml 包含了会深深影响MyBatis行为的设置和属性信息&#xff0c;配置文档的顶层结构如下 environments&#xff08;环境配置&#xff09; environments用于配置数据库的URL信息&#xff0c;MyBatis-config可以动态配置多个数据源&#xff0c;用于连生产、预发、…

(done) 声音信号处理基础知识(4)

来源&#xff1a;https://www.youtube.com/watch?vdaB9naGBVv4 模拟信号特点如下 时域连续(x轴) 振幅连续(y轴) 如下是模拟信号的一个例子&#xff1a; 数字信号特点如下&#xff1a; 一个离散值序列 数据点的值域是一系列有限的值 ADC&#xff1a;模拟信号到数字信号的…

媒体动态:播客增长的重大转变、社交媒体创新和搜索动态

关键亮点&#xff1a; 关键亮点&#xff1a; 电视和音频&#xff1a;播客继续迅速增长&#xff0c;但主要由少数几档节目驱动。付费社交&#xff1a;Meta在最新的一次成功财报电话会议后继续加倍推进AI进展&#xff0c;X起诉GARM和广告商反垄断&#xff0c;Snap的订阅计划继续…

6.网络加密与密钥管理

“网络加密与密钥管理”主要介绍了网络加密与密钥管理的各个方面,包括网络加密的四种模式、密钥管理的基本概念、密钥的生成与分配方法、密钥的保护、存储与备份、层次化密钥管理方法以及密钥管理系统的组成。 四种网络加密模式的原理、特点:链路加密:对网络中两个相邻节点…

OpenGL渲染管线(Rendering Pipeline)介绍

渲染管线 计算机图形学中&#xff0c;计算机图形管线&#xff08;渲染管线 或简称 图形管线、流水线&#xff09;是一个概念模型&#xff0c;它描述了t图像系统将 3D场景渲染到2D屏幕所需执行的一系列步骤。渲染管线大的可以分为三个阶段。 &#xff08;一&#xff09;应用阶段…

基于SpringBoot+Vue+MySQL的手机销售管理系统

系统展示 用户前台界面 管理员后台界面 商家后台界面 系统背景 随着智能手机的普及和市场竞争的日益激烈&#xff0c;手机销售行业面临着前所未有的挑战与机遇。传统的手工记录和简单的电子表格管理方式已难以满足现代手机销售业务的需求&#xff0c;销售数据的混乱和管理效率低…

小程序地图展示poi帖子点击可跳转

小程序地图展示poi帖子点击可跳转 是类似于小红书地图功能的需求 缺点 一个帖子只能有一个点击事件&#xff0c;不适合太复杂的功能&#xff0c;因为一个markers只有一个回调回调中只有markerId可以使用。 需求介绍 页面有地图入口&#xff0c;点开可打开地图界面地图上展…

ftrace - 几种tracer的打印例子

ftrace - Function Tracer — The Linux Kernel documentation【原创】Ftrace使用及实现机制 - 沐多 - 博客园 (cnblogs.com) latency format nop tracer和function tracer下&#xff0c;latency format的时间戳是相对开始trace的时间&#xff0c;non-latency format的时间戳是…

面向对象例题之例题的特性

答案&#xff1a;C 解析&#xff1a;对象里面的方法和属性数量是不确定的&#xff0c;可以不断扩展写多个属性和方法 清洗的边界是对象必备的&#xff0c;哪些是这个类的&#xff0c;哪些是其他类的都有体现。 良好的定义行为一般指定义良好的属性和方法 可扩展性指的是子类…

面向对象设计原则例题

答案&#xff1a;B A C D D C 知识点&#xff1a; 面向对象设计7大原则 单一职责原则 设计目的单一的类 开放—封闭原则 对外扩展开放&#xff0c;对修改封闭 里氏替换原则 子类可以替换父类 依赖倒置原则 要依赖于抽象&#xff0c;而不是具体的实现&#xff1b;针对接…

万字长文——ConvNeXt(2022CVPR),卷积网络的顶峰之作,在Transformer盛行的当下,卷积网络还能再战!

ConvNext:A ConvNet for the 2020s ConvNext:2020 年代的卷积神经网络 论文地址: https://arxiv.org/pdf/2201.03545 自从Transformer成功应用在视觉领域并且取得显著成绩后,很多人开始抛弃卷积网络架构,转而使用Transformer。然而有的大佬不认为卷积过时了,于是有了这篇…