如何实现原生 JS 的拖拽效果

\cb07c717412676904bcf7d9c556e40e4.jpeg

大厂技术  高级前端  Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

前言: 关于“拖拽”,其实是一个老生常谈的需求了,并且还是一个非常经典的面试题。之前在项目中拖拽的场景都是直接使用轮子,虽然很快就能完成设计需求,但是这个的原理一直都是我十分想去深入了解的一部分。

正好在今天的项目中再一次碰到了这个需求,我觉得是时候去探索一下它了。

tips: 本文不会使用 Draggable 去实现,而是会采用原生的 JS 鼠标移动鼠标点击等事件去完成。并且你需要明确知道的一点是:🎁本文的最终目的并不是实现一个开箱即用的轮子,而是让你理解拖拽实现的原理,知其然并知其所以然。 希望你可以有耐心和我一起完成下面的功能。

我们先看一下预览效果:

一. 前期准备

  1. 这个需求实现要准备的文件很少,你只需要创建一个 .vue 文件即可快速开始接下来的实现,你可以自己动手写出下面的样式,也可以跳到源码标题复制我的样式来快速进行下一步。
    cba47522b318714e5c482db624ef767d.png

  2. 样式方面,在这里我使用的是 UnoCSS ,将样式內联在了标签里,如果你还不了解这种写法,你可以点击下方的文章学习。不过即使你之前从未了解过 UnoCSS ,也不会影响你下面的阅读,因为样式不是本文的重点,并不影响整体阅读。
    🫱手把手教你如何创建一个代码仓库[2]

  3. 在这里我们简化一下,我们暂时去掉不重要的 hover 动画的影响,直接切入主题 “拖拽”

  4. 注意:为了减少出现大量的属性名导致本文理解起来难度会有些许提升的缘故,在这里我们暂时不牵扯 Y 轴上的拖拽效果。你也不必担心,因为它和 X 轴上的移动原理是完全一样的,还希望读者学习之后可以自行推导出。

二. clickmouseDownmouseUp 的区别

  1. 首先用户想要完成拖拽这一操作,它的动作里肯定包含了鼠标按下的这个动作。在这里比较容易和 click 事件搞混。首先我们要知道 click 事件是包含两个动作的。一个是用户鼠标按下,一个是用户鼠标抬起。这两个关键动作如果在一起则构成了我们的 click 事件。
    1dd8ddfecd444faaf4ef3333ef676d30.jpeg

  2. 在这里我们补充一个额外的知识。注意上面划红线的一句话:

    click 事件会在 `mousedown`[3] 和 `mouseup`[4] 事件依次触发后触发。”

    其实理解起来也很简单,就是当你同时给一个元素添加 clickmouseDownmouseup 的时候。虽然看起来 click 好像是由另外两个事件组成的,但其实它们三个是相互独立的事件。并且 click 的优先级会低一点点,会在这两个事件执行完毕之后再去执行。

  3. 验证一下,我们直接先给滑块一个绑定这三个事件。6591b8aa26bf6df556803251efff3106.jpeg
    我们看一下控制台的输出顺序:
    643d77b39401ae2654f91f0e4af8ba71.jpeg
    说明我们上面的结论是没问题的。

三. clientX 是什么?

  1. 不过我们今天的主角是 onMousedown 这个事件,所以我们暂时先把另外两位请下台稍作休息。
    7cddefa3779836b32ee5c4b362eb8f45.jpeg

  2. 我们点击一下这个元素,在这里我们需要用到当前点击传递过来的事件对象身上的一些属性。
    其实拖拽的关键点就在于如何利用这些属性来动态改变滑块的位置。

  3. 在这里我们选择使用 clientX 这个属性。这里如果大家对其它关于 X 的属性不了解的话,还希望自行去了解一下,不属于本文的范畴。你可以点击这里去了解其它属性的含义。
    🫱你必须知道的关于坐标轴的属性[5]
    在这里我简单介绍一下 clientX 代表的含义。
    79e7c301cac740e250ac29ea22cd3386.jpeg
    假设我在滑块黄色圆点处点击了一下,那么从可视区域的范围内的最左侧开始到这个黄点的距离就是 clientX
    为什么我在这里要强调可视区域呢?我在下面的文章里已经做出了非常详细的介绍,感兴趣的话可以自行查阅。
    [🫱你必须知道的 clientWidth, offsetWidth, scrollWidth](juejin.cn/post/719612…[6]

  4. 这个属性对我们来说非常关键,聪明的你已经猜到了,它其实就代表着我们拖拽的起点坐标,这里我们需要把它保存到一个变量里。
    968a03e3f449a376a663344c9fc7fced.jpeg

四. onMouseMove 和 onMouseUp 的使用

  1. 和上面的代码大同小异,这里我就不过多赘述。
    0d21554d1cacd01cdc6abdd3b9af683d.jpeg

  2. 绑定这个事件之后,我们会发现当我鼠标在滑块内移动的时候,它就会执行。
    d8319144c0dce004249a365d8c46ead7.jpeg

  3. 但是这个效果并不是我们想要的,我们想要的是当我们鼠标按下的时候你开始记录就可以了,不需要触发的这么频繁。要达成也非常简单,增加一个中间变量 isDown,来记录这个状态即可。那么随之就需要搭配我们的 onMouseDownonMouseUp 来共同维护这个变量。
    a6ad86fb28b1f778a54d899fab1ad736.jpeg
    我把这个变量值直接显示在页面上,接下来我们测试一下:

    可以看到已经暂时达到我们的需求。

  4. 到目前为止我们的实现其实存在一个 bug。具体看下面:
    64661b166d399dde23bcfdc977fe2f43.jpeg
    细心的读者可能已经发现了一个问题,当我在滑块内部按下鼠标后 isDown 的值变为了 true,但是当我鼠标划出滑块内部然后抬起的时候,mouseup 事件并没有被正确的执行。
    225a40922f7803326afa278abbcd2237.jpeg

  5. 最开始我在这里迷惑了很久🤔,去 MDN 查阅相关事件的时候,并没有发现任何相关的解释。
    5c164d6853beacd3e68f32e96c92e130.jpeg

  6. 但是我突然注意到了之前看到 Click 事件上的一段解释。
    34c2f6ef520bffb2dc96ef28f6739e11.jpeg

  7. 由这句话我猜想是否应该把这个 onMouseUp 上移到最外层的元素上来呢?🤔 说干就干。
    d6b88f8720ee5dbb1abe0cda1273be96.jpeg
    然后我们验证一下:
    487dcb929189482eb59d4f4f9371d26f.jpeg
    嗯~现在我们的代码应该是没什么问题了,可以接着进行下一步了。

  8. 这里或许会有小伙伴迷惑,那我如果不在滑块外面松开了,我依旧在滑块内部松开呢?我们先验证一下:
    64ec0018c045d16ca85004cbdc8608f8.jpeg
    可以看出,是丝毫不影响我们的效果的。
    奇怪🤔,这是为什么呢?

  9. 我们首先给滑块一个不一样的 onMouseUp 事件。2fd91181c5c086c483ff9da72650e328.jpeg
    a299532bc56367c1eded778fca9b53a5.jpeg
    经过上面的实验,我猜你已经发现了,其实非常简单,就是因为事件冒泡的机制。虽然我们在滑块内部松开了鼠标,但是由于事件冒泡,最外层 divonMouseUp 事件也被触发了,所以正确的设置了 isDown 的状态。

五. 拖拽效果的原理

  1. 解决了边界问题,那么我们现在就可以放心地去完成拖拽的效果了,别着急写代码。首先让我们分析一下拖拽的原理到底是什么?

  2. 假设我在滑块内部鼠标按下后,拖拽了一段距离然后松开了鼠标。我们用下图的起点终点分别代表这两个事件。
    831a8d03469d035ead4658bb8628a7ee.jpeg

  3. 然后我们结合我们上面提到的关键属性 clientX
    e3ec0dae247d7c47aeae42553059dc2f.jpeg
    可以看出,我们滑块滑动的距离其实就是 clienX值。
    bdcf742767db793f812e9685c45dec37.jpeg

  4. 关键问题就来了,如何得出这个差值?其实非常非常简单,我们的 onMouseMove 会被传递的那个事件对象上也存在一个 clientX属性,那我的起点坐标信息有了,这两者相剪不就是我们想要的结果吗?
    2b9ce92d5e03be1ae13e6dda2a6add66.jpeg
    2e6fec95e4a68a7a327d8590c704c094.jpeg

六. 拖拽效果的实现

  1. 移动的距离有了,那么接下来就是如何将这个滑块动起来了,这里我查阅了两种方式,我们先介绍第一种。主要思路为将滑块更换为 absolute 布局,然后更改 left 值来完成。这里我们先简单实现一下,然后再讲解它的弊端。

  2. 我们先给滑块打上 ref,因为之后我们要借助 JS 去操作这个元素节点。
    f3a15f32d39f0e3cf1b9da470162390a.jpeg
    c69adf61774523a76d4c0a5aaadf70e2.jpeg

  3. 思路非常清晰,当我们鼠标按下(onMouseDown)的时候,要给滑块设置 absolute83bfe8f0380bd45f7ac0dd077356f9c6.jpeg

  4. 鼠标移动onMouseMove)的时候,将滑块 left 的值修改为差值
    febf5bc77ebc1c7ee7fd7862234cdfdf.jpeg

  5. 对了,别忘了需要给滑块滑动范围的外壳 div 设置 relative 属性。
    1663e2f4cc6604c2d66b5c2ace9701ec.jpeg

  6. 到这里我们其实就可以看到简单的效果了。
    99fd5b4090ec0b671f567b3c525a540a.jpeg

  7. 但是目前还会出现一个问题,如果我在滑动的时候松手,然后重新拖拽的时候,滑块会从头开始。
    ec1a031119f89d68e68ccdec557d3965.jpeg

  8. 造成这个情况的原因也很简单,理想情况下,假设你在中间松手之后重新拖拽了 10px 的距离。1e109a12440be27c87576b60391a2fb3.jpeg
    那么根据我们现在的逻辑,其实你刚刚移动了 1px 的时候,我们的代码马上执行了 onMouseMove 函数。
    bd677fcfe5880d859db7ff4c6821899e.jpeg
    那么它会马上设置我们滑块的 left1px,就造成了滑块马上回到了起点的现象。

  9. 解决方法也很简单,当鼠标按下的时候,拿到起始的 left 值即可。
    2fff9b9c34728a9ed735dcf5a48d37bd.jpeg
    然后我们在鼠标移动差值之前每次都加上初始值就 ok 了。
    d6bb77abf77dacabf36bebacf8654596.jpeg
    我们看一下效果:
    58bbfd6355f362fae8a704f4636c4845.jpeg

七. 更优雅的拖拽方案

  1. 在上面我们使用到了 absolute 定位,并且重复修改 left 的值。其实这样的操作是会引起页面的重排。在性能方面上的考虑来讲,我们可以采取搭配 tansform 来去操作这个移动的效果,对性能方面考虑来讲是更优的选择。

  2. 并且实现起来更加简单,我们只需要在滑块移动的时候修改 tansform 属性的 tanslateX 即可。
    c26495fbffd2ea7aeac73177c257ab88.jpeg
    效果如下:
    add63bd46f1f0a80574a9ead5f5a5d50.jpeg 只是目前还是会出现在中间松手,然后重新拖拽会返回起点的情况,造成的原因和上面 absolute 的情况一样,都是需要加上初始的值。

  3. 但是这里获取初始值的方法不太一样。由于我们第一次调取 onMouseDown 的时候,我们的 onMouseMove 事件其实还没触发,所以我们的 transform 属性有可能为 字符串String格式的 null。并且这里需要特别注意的一点是,我们拿到的 tansform 属性是一个 matrix 函数的字符串表示形式。它并不是我们理想状态下的 tansformX = 110 px 等这样现成可以使用的值。
    83c2d599591ec54e6a93bdc8a89873f3.jpeg

  4. 这里我们如果要是使用的话的话,需要自己去通过字符串的一些方法去自行切割。1e4b696af8e2c3cbff2ed6324b9340d0.jpeg
    而我们想要的数据就是切割好的数组中的第五个。
    63e80d164cabe855d51502221a25bb60.jpeg

  5. 那么对应的,在 onMouseMove 函数中直接使用即可。
    4c705162a2f523df832ec2e3bafde471.jpeg
    这是页面的效果:
    7e718a8f54db4a06f629a549e980e7b9.jpeg

七. 源码

<script setup lang="ts">
import { ref } from "vue";

const slider = ref<HTMLDivElement>();

const startPoint = ref<number>(0);
const isDown = ref<boolean>(false);

const premitiveX = ref<number>(0);

function onMouseDown(e: any) {
  isDown.value = true;
  const style = window.getComputedStyle(slider.value!);
  const { transform } = style;
  if (transform !== "none") {
    const matrixArr = transform.replace(/[^0-9\-,]/g, "").split(",");
    console.log("matrixArr", matrixArr);
    premitiveX.value = parseInt(matrixArr[4]);
  } else {
    premitiveX.value = 0;
  }
  const { clientX } = e;
  startPoint.value = clientX;
}

function onMosueUp(e: any) {
  isDown.value = false;
}

function onMouseMove(e: any) {
  if (!isDown.value) return;
  const { clientX } = e;
  const moveDistance = clientX - startPoint.value;
  const offset = premitiveX.value + moveDistance;
  console.log("offset", offset);
  slider.value!.style.transform = `translateX(${offset}px)`;
}
</script>

<template>
  <div @mouseup="onMosueUp" class="w-100vw h-100vh bg-blue flex items-center">
    <div class="w-500px h-200px bg-black flex ml-100px relative">
      <div
        ref="slider"
        @mousedown="onMouseDown"
        @mousemove="onMouseMove"
        class="w-100px h-full border-white border-4px"
      >
        <span class="text-50px">滑块</span>
      </div>
    </div>
  </div>
</template>
<style></style>
复制代码

总结

最开始写这个解锁效果的时候,其实也查阅了很多教程,大部分都是直接教你如何使用 H5 draggble 这个标签去实现的,但是我就在想 H5 之前人们是如何使用这个拖拽的呢?于是就自己去思考和动手尝试,最终才有了这篇文章。

随之几天我也会重新更新一篇使用 draggable 实现拖拽效果的文章,还是会秉持着通俗易懂的语言来和你一起学习这个知识点。与君共勉才是我写作的真正目的。

赠人玫瑰手有余香~🌹

关于本文

作者:韩振方

https://juejin.cn/post/7204316982887514169

 
 

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

c2ab79b3deb7c194a241716c63728473.jpeg

“分享、点赞、在看” 支持一波👍
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值