如何实现原生 JS 的拖拽效果
创作时间:
作者:
@小白创作中心
如何实现原生 JS 的拖拽效果
引用
CSDN
1.
https://blog.csdn.net/2401_83384536/article/details/140323379
前言: 关于“拖拽”,其实是一个老生常谈的需求了,并且还是一个非常经典的面试题。之前在项目中拖拽的场景都是直接使用轮子,虽然很快就能完成设计需求,但是这个的原理一直都是我十分想去深入了解的一部分。
正好在今天的项目中再一次碰到了这个需求,我觉得是时候去探索一下它了。
tips: 本文不会使用
Draggable
去实现,而是会采用原生的 JS 鼠标移动,鼠标点击等事件去完成。并且你需要明确知道的一点是:🎁本文的最终目的并不是实现一个开箱即用的轮子,而是让你理解拖拽实现的原理,知其然并知其所以然。 希望你可以有耐心和我一起完成下面的功能。
一. 前期准备
- 这个需求实现要准备的文件很少,你只需要创建一个
.vue
文件即可快速开始接下来的实现,你可以自己动手写出下面的样式,也可以跳到源码标题复制我的样式来快速进行下一步。 - 样式方面,在这里我使用的是
UnoCSS
,将样式內联在了标签里,如果你还不了解这种写法,你可以点击下方的文章学习。不过即使你之前从未了解过
UnoCSS
,也不会影响你下面的阅读,因为样式不是本文的重点,并不影响整体阅读。 - 在这里我们简化一下,我们暂时去掉不重要的
hover
动画的影响,直接切入主题 “拖拽”。 - 注意:为了减少出现大量的属性名导致本文理解起来难度会有些许提升的缘故,在这里我们暂时不牵扯 Y 轴上的拖拽效果。你也不必担心,因为它和 X 轴上的移动原理是完全一样的,还希望读者学习之后可以自行推导出。
二. click 和 mouseDown 和 mouseUp 的区别
- 首先用户想要完成拖拽这一操作,它的动作里肯定包含了鼠标按下的这个动作。在这里比较容易和 click 事件搞混。首先我们要知道 click 事件是包含两个动作的。一个是用户鼠标按下,一个是用户鼠标抬起。这两个关键动作如果在一起则构成了我们的 click 事件。
- 在这里我们补充一个额外的知识。注意上面划红线的一句话:
“
click
事件会在mousedown
[3] 和mouseup
[4] 事件依次触发后触发。”
其实理解起来也很简单,就是当你同时给一个元素添加
click
和
mouseDown
和
mouseup
的时候。虽然看起来
click
好像是由另外两个事件组成的,但其实它们三个是相互独立的事件。并且
click
的优先级会低一点点,会在这两个事件执行完毕之后再去执行。 - 验证一下,我们直接先给滑块一个绑定这三个事件。
我们看一下控制台的输出顺序:
三. clientX 是什么?
- 不过我们今天的主角是
onMousedown
这个事件,所以我们暂时先把另外两位请下台稍作休息。 - 我们点击一下这个元素,在这里我们需要用到当前点击传递过来的事件对象身上的一些属性。
其实拖拽的关键点就在于如何利用这些属性来动态改变滑块的位置。 - 在这里我们选择使用
clientX
这个属性。这里如果大家对其它关于 X 的属性不了解的话,还希望自行去了解一下,不属于本文的范畴。你可以点击这里去了解其它属性的含义。 - 这个属性对我们来说非常关键,聪明的你已经猜到了,它其实就代表着我们拖拽的起点坐标,这里我们需要把它保存到一个变量里。
四. onMouseMove 和 onMouseUp 的使用
- 和上面的代码大同小异,这里我就不过多赘述。
- 绑定这个事件之后,我们会发现当我鼠标在滑块内移动的时候,它就会执行。
- 但是这个效果并不是我们想要的,我们想要的是当我们鼠标按下的时候你开始记录就可以了,不需要触发的这么频繁。要达成也非常简单,增加一个中间变量
isDown
,来记录这个状态即可。那么随之就需要搭配我们的
onMouseDown
和
onMouseUp
来共同维护这个变量。
我把这个变量值直接显示在页面上,接下来我们测试一下:
可以看到已经暂时达到我们的需求。 - 到目前为止我们的实现其实存在一个 bug。具体看下面:
细心的读者可能已经发现了一个问题,当我在滑块内部按下鼠标后 isDown 的值变为了
true
,但是当我鼠标划出滑块内部然后抬起的时候,mouseup 事件并没有被正确的执行。 - 最开始我在这里迷惑了很久🤔,去 MDN 查阅相关事件的时候,并没有发现任何相关的解释。
- 但是我突然注意到了之前看到
Click
事件上的一段解释。 - 由这句话我猜想是否应该把这个
onMouseUp
上移到最外层的元素上来呢?🤔 说干就干。
然后我们验证一下:
嗯~现在我们的代码应该是没什么问题了,可以接着进行下一步了。 - 这里或许会有小伙伴迷惑,那我如果不在滑块外面松开了,我依旧在滑块内部松开呢?我们先验证一下:
可以看出,是丝毫不影响我们的效果的。
奇怪🤔,这是为什么呢? - 我们首先给滑块一个不一样的
onMouseUp
事件。
经过上面的实验,我猜你已经发现了,其实非常简单,就是因为事件冒泡的机制。虽然我们在滑块内部松开了鼠标,但是由于事件冒泡,最外层 div 的
onMouseUp
事件也被触发了,所以正确的设置了
isDown
的状态。
五. 拖拽效果的原理
- 解决了边界问题,那么我们现在就可以放心地去完成拖拽的效果了,别着急写代码。首先让我们分析一下拖拽的原理到底是什么?
- 假设我在滑块内部鼠标按下后,拖拽了一段距离然后松开了鼠标。我们用下图的起点和终点分别代表这两个事件。
- 然后我们结合我们上面提到的关键属性
clientX
。
可以看出,我们滑块滑动的距离其实就是
clienX
的差值。 - 关键问题就来了,如何得出这个差值?其实非常非常简单,我们的
onMouseMove
会被传递的那个事件对象上也存在一个
clientX
属性,那我的起点坐标信息有了,这两者相剪不就是我们想要的结果吗?
六. 拖拽效果的实现
- 移动的距离有了,那么接下来就是如何将这个滑块动起来了,这里我查阅了两种方式,我们先介绍第一种。主要思路为将滑块更换为
absolute
布局,然后更改
left
值来完成。这里我们先简单实现一下,然后再讲解它的弊端。 - 我们先给滑块打上 ref,因为之后我们要借助 JS 去操作这个元素节点。
- 思路非常清晰,当我们鼠标按下(onMouseDown)的时候,要给滑块设置
absolute
。 - 当鼠标移动(onMouseMove)的时候,将滑块
left
的值修改为差值。 - 对了,别忘了需要给滑块滑动范围的外壳 div 设置 relative 属性。
- 到这里我们其实就可以看到简单的效果了。
- 但是目前还会出现一个问题,如果我在滑动的时候松手,然后重新拖拽的时候,滑块会从头开始。
- 造成这个情况的原因也很简单,理想情况下,假设你在中间松手之后重新拖拽了 10px 的距离。
那么根据我们现在的逻辑,其实你刚刚移动了 1px 的时候,我们的代码马上执行了
onMouseMove
函数。
那么它会马上设置我们滑块的
left
为 1px,就造成了滑块马上回到了起点的现象。 - 解决方法也很简单,当鼠标按下的时候,拿到起始的 left 值即可。
然后我们在鼠标移动的差值之前每次都加上初始值就 ok 了。
我们看一下效果:
七. 更优雅的拖拽方案
- 在上面我们使用到了
absolute
定位,并且重复修改
left
的值。其实这样的操作是会引起页面的重排。在性能方面上的考虑来讲,我们可以采取搭配
tansform
来去操作这个移动的效果,对性能方面考虑来讲是更优的选择。 - 并且实现起来更加简单,我们只需要在滑块移动的时候修改
tansform
属性的
tanslateX
即可。
效果如下:
只是目前还是会出现在中间松手,然后重新拖拽会返回起点的情况,造成的原因和上面
absolute
的情况一样,都是需要加上初始的值。 - 但是这里获取初始值的方法不太一样。由于我们第一次调取
onMouseDown
的时候,我们的
onMouseMove
事件其实还没触发,所以我们的
transform
属性有可能为 字符串String格式的 null。并且这里需要特别注意的一点是,我们拿到的
tansform
属性是一个 matrix 函数的字符串表示形式。它并不是我们理想状态下的
tansformX = 110 px
等这样现成可以使用的值。 - 这里我们如果要是使用的话的话,需要自己去通过字符串的一些方法去自行切割。
而我们想要的数据就是切割好的数组中的第五个。 - 那么对应的,在
onMouseMove
函数中直接使用即可。
这是页面的效果:
七. 源码
<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
实现拖拽效果的文章,还是会秉持着通俗易懂的语言来和你一起学习这个知识点。与君共勉才是我写作的真正目的。
赠人玫瑰手有余香~🌹
热门推荐
吃完饭就犯困或是疾病信号,揭开饭后困倦的神秘面纱
VSM用于Ni-Zn纳米铁氧体的微观结构、光学和磁学研究
龙珠:在动漫届处于什么地位?
酥皮奶油泡芙制作全攻略:从基础到进阶的完整指南
头顶痛最好的缓解方法
QDII基金如何应对市场变化?这种市场变化对基金有何影响?
通报:10月商家投诉电商平台典型案例曝光——小红书
12岁孩子有白头发是怎么回事
研究人性最透彻的书
多模态融合技术现实世界中的挑战与研究进展
王勃的《滕王阁序》,到底是怎么个好法,为何能惊艳后世千余年
酸枣仁的功效与作用:从传统应用到现代研究
表格如何清晰展示数据库
越来越多年轻人中招的飞蚊症,到底是啥?
民族舞剧《红楼梦》:根植于中华经典的沃土
适合血糖高的人喝的蔬菜汤做法
Excel导出PDF不清晰怎么办?一文详解设置技巧
《上古卷轴5:天际重制版》黑暗精灵历史故事介绍
手动挡停车时应先踩离合还是先踩刹车?
何时使用计算列和计算字段
2025年英语四六级考试全攻略:从报名到通关,一篇说清!
F1中国大奖赛“擎”动上海 赛事与产业“双向奔赴”
什么是多做空?详解这一金融投资策略
Curr Obes Rep:减肥手术对青少年的短期和长期影响
AI时代,视障者如何“数字追光”?
因为共情 所以共鸣——清明节的世界文化共振
伤口是局部,影响是身心整体
MidJourney 常用提示词与图片链接的全面指南
中国5个“夜生活最丰富”的城市,凌晨2点满街人,有你的家乡吗?
6首最美桃花诗词:桃之夭夭,惊艳了整个春天