拖拽组件
一、元素位置变化的基础原理
元素移动的核心在于改变其在坐标系中的位置,大致思路是鼠标移动后的位置-鼠标移动前的位置 + 元素移动前的位置 = 元素移动后的位置,CSS 提供了多种改变位置的方式,以下是transform、position、margin这几种位置变化的方式、可能出现的问题以及使用比较
使用transform:translate
1const handleEle = document.getElementById('handler');
2 let isDragging = false;
3 let offsetX, offsetY;
4 let mouseBegin = {
5 x: undefined,
6 y: undefined
7 }
8 let eleBegin = {
9 x: undefined,
10 y: undefined
11 }
12 // 鼠标移动后的位置-鼠标移动前的位置 + 元素移动前的位置 = 元素移动后的位置
13 // 错误写法
14 handleEle.addEventListener('mousedown', (e) => {
15 isDragging = true;
16 const rect = handleEle.getBoundingClientRect();
17 mouseBegin.x = e.x;
18 mouseBegin.y = e.y;
19 eleBegin.x = rect.left;
20 eleBegin.y = rect.top;
21 console.log(eleBegin.x, eleBegin.y, mouseBegin);
22
23 });
24
25 document.addEventListener('mousemove', (e) => {
26 if (!isDragging) return;
27 offsetX = e.x - mouseBegin.x;
28 offsetY = e.y - mouseBegin.y;
29
30 handleEle.style.transform = `translate(${eleBegin.x + offsetX}px, ${eleBegin.y + offsetY}px)`;
31 });
32
33 document.addEventListener('mouseup', () => {
34 isDragging = false;
35 });
可能出现的问题:
鼠标放上去后元素会向下抖动,如果增加一个元素效果更明显
1<div id="handler1">handler ele1</div>
2<div id="handler2">handler ele2</div>
问题原因:
CSS对象模型视图是连接CSS样式与JavaScript交互的核心接口,它定义了如何通过JavaScript访问和操作元素的视觉呈现属性,该模型包含多层次的坐标系体系。在元素拖拽移动中,坐标系是元素可以移动的基础,定义了translate变换的元素会创建一个新的局部坐标系,这个坐标系和原始坐标系在坐标系原点和定位基准等有所不同
正确实现方案eleBegin = { x: matrix.m41, y: matrix.m42 }
1handleEle.addEventListener('mousedown', (e) => {
2 isDragging = true;
3 const rect = handleEle.getBoundingClientRect();
4 mouseBegin.x = e.x;
5 mouseBegin.y = e.y;
6 const style = window.getComputedStyle(handleEle);
7 const matrix = new DOMMatrix(style.transform);
8 // 重点是这里
9 eleBegin = { x: matrix.m41, y: matrix.m42 };
10
11 });
12
13 document.addEventListener('mousemove', (e) => {
14 if (!isDragging) return;
15 offsetX = e.x - mouseBegin.x;
16 offsetY = e.y - mouseBegin.y;
17
18 handleEle.style.transform = `translate(${eleBegin.x + offsetX}px, ${eleBegin.y + offsetY}px)`;
19 });
20 document.addEventListener('mouseup', () => {
21 isDragging = false;
22 });
使用translate的特点:
- 不会触发重排(reflow),只引发重绘(repaint)
- 不影响文档流和其他元素布局
- GPU加速,性能最佳
- 坐标计算相对简单
使用position:absolute
1handleEle.addEventListener('mousedown', (e) => {
2 isDragging = true;
3 const pos = handleEle.getBoundingClientRect();
4 mouseBegin = { x: e.x, y: e.y };
5 eleBegin = {x: pos.left, y: pos.top };
6 console.log('eleBegin', eleBegin);
7 });
8
9 document.addEventListener('mousemove', (e) => {
10 if (!isDragging) return;
11 offsetX = e.x - mouseBegin.x;
12 offsetY = e.y - mouseBegin.y;
13 handleEle.style.left = `${eleBegin.x + offsetX}px`;
14 handleEle.style.top = `${eleBegin.y + offsetY}px`;
15 });
同理,positon为absolute时,他的坐标系是基于最近的定位元素来计算的,而不是基于浏览器视口的位置,解决办法可以是通过offsetLeft和offsetTop来获取定位元素的位置
1// CSS 需要预先设置
2// .draggable { position: absolute; }
3
4 handleEle.addEventListener('mousedown', (e) => {
5 isDragging = true;
6 mouseBegin = { x: e.x, y: e.y };
7 // 关键步骤
8 eleBegin = {x: handleEle.offsetLeft, y: handleEle.offsetTop };
9 console.log('eleBegin', eleBegin);
10 });
11
12 document.addEventListener('mousemove', (e) => {
13 if (!isDragging) return;
14 offsetX = e.x - mouseBegin.x;
15 offsetY = e.y - mouseBegin.y;
16 handleEle.style.left = `${eleBegin.x + offsetX}px`;
17 handleEle.style.top = `${eleBegin.y + offsetY}px`;
18 });
使用absolute特点:
- 从文档流中脱离
- 需要父元素有定位上下文(非static)
- 会触发重排,性能中等
- 坐标计算需要考虑offsetParent
使用margin
1<div id="handler1">handle1</div>
2<div id="handler2">handle2</div>
3<div>other ele</div>
1let isDragging = false;
2let startX, startY, startMarginLeft, startMarginTop;
3
4element.addEventListener('mousedown', (e) => {
5 isDragging = true;
6 startX = e.clientX;
7 startY = e.clientY;
8 startMarginLeft = parseInt(getComputedStyle(element).marginLeft) || 0;
9 startMarginTop = parseInt(getComputedStyle(element).marginTop) || 0;
10 e.preventDefault();
11});
12
13document.addEventListener('mousemove', (e) => {
14 if (!isDragging) return;
15 element.style.marginLeft = `${startMarginLeft + e.clientX - startX}px`;
16 element.style.marginTop = `${startMarginTop + e.clientY - startY}px`;
17});
18
19document.addEventListener('mouseup', () => {
20 isDragging = false;
21});
特点:
- 保持在文档流中
- 会影响周围元素布局
- 性能最差(触发完整重排)
- 实际项目中很少用于拖拽实现
- 布局流变化
1.parent {
2 width: 80%; /* 视口变化时影响子元素 */
3}
- 布局模式变化
1.element {
2 display: inline-block; /* 从block切换会改变文档流 */
3}
脱离常规流但仍影响周围元素
1.element {
2 float: left; /* 导致后续元素环绕 */
3}
- 高级布局影响
- flex/grid容器调整,容器属性变化导致子项重新分配空间
1.container {
2 justify-content: space-between; /* 修改后子项位置重计算 */
3}
1.element {
2 order: 2; /* 改变在Flex容器中的显示顺序 */
3}
2.4 三种方法对比
特性 |
transform: translate |
position: absolute |
margin |
文档流影响 |
无 |
脱离 |
保持 |
性能 |
最佳(GPU加速) |
中等 |
最差 |
坐标计算复杂度 |
中等 |
中等 |
简单 |
适用场景 |
现代UI、动画 |
传统拖拽 |
不推荐 |
层叠上下文 |
创建 |
可能创建 |
不创建 |
响应式布局兼容性 |
好 |
一般 |
差 |
结论
理解CSS坐标系是实现元素拖拽的基础,在改变元素位置时,要考虑是否在同一个坐标系。而 transform: translate 因其优异的性能和简洁的实现方式,已成为现代Web开发中实现拖拽功能的首选方案。对于需要精确控制层叠上下文或兼容旧浏览器的场景,可考虑使用position: absolute 方案,而margin方案由于其性能缺陷,在实际开发中应避免用于拖拽实现。