最小化事件侦听器的数量以实现 "Draggable" 行为
Minimizing the number of event listeners to achieve a "Draggable" behaviour
Objective
试图复制 jQueryUI 的 .draggable
行为。
$(function() {
$(".box").draggable();
});
.box {
width: 100px;
height: 100px;
background: pink;
}
<head>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
</head>
<body>
<div class="box"></div>
</body>
一个不是很多可行的解决方案
我创建了一个函数 draggable
来执行此操作。它接受一个 HTMLElement
类型的对象作为参数:需要拖动的元素。函数需要遵循以下两点:
可以在多个元素上调用该函数,从而可以拖动页面上的多个元素(像jQueryUI那样一次只能拖动一个元素)
可以将可拖动元素拖动到页面上用户希望移动的任何位置(更准确地说是在父容器中)。
我已经开始编写函数了,它的工作原理如下:
它在 元素上添加一个 onmousedown
事件侦听器
当事件被触发时,从鼠标位置到被拖动元素角的距离(在x
和y
上)保存在relativeX
和[=27=内]. dragging
的状态也从 false
更改为 true
。
它在 document
上添加了一个 onmousemove
事件侦听器
当用户的鼠标在页面上移动时,该事件不断被触发。这允许检查 dragging
的状态是否为 true
。当状态确实为 true
时,表示用户正在拖动元素。因此需要更新元素的位置。考虑到相对位置(之前保存的 relativeX
和 relativeY
),我使用绝对定位来使这些位置正确。
它在 document
上添加 onmouseleave
和 onmouseup
以在需要时结束行为:
当用户停止拖动元素(鼠标左键 向上)或离开 document
时,函数会将 dragging
的状态更改回 false
。因此 onmousemove
监听器将继续监听但不会更新位置,直到 dragging
再次设置回 true
。
下面是更好说明的代码:
const relativePos = (e, p) => {
let rect = e.target.getBoundingClientRect()
return [e.clientX - rect.x, e.clientY - rect.y];
}
const draggable = draggedE => {
let isDragging, relativeX, relativeY;
// checking if the user is dragging
document.onmousemove = (event) => {
if (isDragging) {
draggedE.style.left = `${event.clientX - relativeX}px`
draggedE.style.top = `${event.clientY - relativeY}px`
}
}
// when the user initiate the dragging behaviour
draggedE.onmousedown = e => {
[isDragging, relativeX, relativeY] = [true, ...relativePos(e)]
}
// listener to end the behaviour
document.onmouseup = () => isDragging = false
document.onmouseleave = () => isDragging = false
}
draggable(document.querySelector('.box'))
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
}
<div class="box"></div>
事件侦听器有问题
代码完美运行。 但这是我的担忧:
我使用了很多可拖动元素。对于它们中的每一个,都有一个事件附加到文档以检查 dragging
的状态。这似乎对性能不太好。
此外,如果您仔细观察下面的动画(使用 jQueryUI 的 .draggable
方法):
您可以看到我可以在不停止行为的情况下离开页面:我可以将元素拖出页面。我发现我只能通过将我的听众附加到 document
来实现这样的结果。将它附加到父元素甚至 body
都不起作用。
我的问题
如何在不使用那么多事件处理程序(在父级上,在 document
上更糟)的情况下实现上述结果,同时仍确保相同的行为?
[编辑] 可能的解决方案
Teemu建议:
我创建了一个包含所有方法的对象draggable
在 document
上添加唯一的 mousedown
事件处理程序
这是它的样子:
const draggable = {
startDrag: function(event) {
this.element = event.target
const rect = event.target.getBoundingClientRect();
this.offset = [event.clientX - rect.x, event.clientY - rect.y];
},
dragging: function(e) {
this.element.style.left = `${e.clientX - this.offset[0]}px`;
this.element.style.top = `${e.clientY - this.offset[1]}px`;
},
endDrag: function() {},
element: '',
offset: []
};
document.addEventListener('mousedown', e => {
if (e.target.classList.contains('draggable')) {
draggable.startDrag(e)
const _mousemove = draggable.dragging.bind(draggable)
document.addEventListener('mousemove', _mousemove);
document.addEventListener('mouseup', function _mouseup(event) {
document.removeEventListener('mousemove', _mousemove)
document.removeEventListener('mouseup', _mouseup)
});
}
});
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
user-select: none;
}
.box:nth-child(1) {
background: lightblue;
top: 125px;
left: 125px;
}
.box:nth-child(2) {
background: lightgreen;
top: 250px;
left: 250px;
}
<div class="draggable box"></div>
<div class="draggable box"></div>
<div class="draggable box"></div>
必须命名处理程序使用的函数以便以后删除它们让我很烦恼。 有没有其他方法可以在不调用 bind
的情况下删除事件?
感谢 Teemu 我找到了解决问题的方法。这个想法是在用户不拖动时 运行 一个唯一的事件监听器。当他拖动一个元素时,其他事件侦听器将被添加 并在用户结束拖动过程后立即被删除 使用 mouseup
或 mouseleave
.
draggable
是一个包含所有函数的对象:startDrag
、dragging
和 endDrag
以及信息:element
和 offset
关于拖动机制。
为了删除事件侦听器,我需要将我的事件函数保存在变量中。
此外,我必须使用 bind()
才能访问 draggable
在不同函数中的属性。所以我想出了 属性 events
,其中充满了 draggable
的函数绑定到 draggable
...
所以我想出了:
(function() {
this.events = {
_mousemove: this.dragging.bind(this),
_mouseup: this.endDrag.bind(this)
}
}.bind(draggable))()
而不是写作:
const _mousemove = draggable.dragging.bind(draggable)
const _mouseup = draggable.endDrag.bind(draggable)
所以现在我可以通过 this.events._mousemove
和 this.events._mouseup
.[=36 访问链接到 _mousemove
和 _mouseup
内部的事件来停止 endDrag
=]
现在可以添加事件侦听器:
document.addEventListener('mousemove', draggable.events._mousemove);
document.addEventListener('mouseup', draggable.events._mouseup);
这是完整的 JavaScript 代码:
const draggable = {
events: {},
startDrag: function(event) {
this.element = event.target
const rect = event.target.getBoundingClientRect();
this.offset = [event.clientX - rect.x, event.clientY - rect.y];
},
dragging: function(e) {
this.element.style.left = `${e.clientX - this.offset[0]}px`;
this.element.style.top = `${e.clientY - this.offset[1]}px`;
},
endDrag: function() {
document.removeEventListener('mousemove', this.events._mousemove)
document.removeEventListener('mouseup', this.events._mouseup)
},
element: '',
offset: []
};
document.addEventListener('mousedown', e => {
(function() {
this.events = {
_mousemove: this.dragging.bind(this),
_mouseup: this.endDrag.bind(this)
}
}.bind(draggable))()
if (e.target.classList.contains('draggable')) {
draggable.startDrag(e)
document.addEventListener('mousemove', draggable.events._mousemove);
document.addEventListener('mouseup', draggable.events._mouseup);
}
});
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
user-select: none;
}
.box:nth-child(1) {
background: lightblue;
top: 125px;
left: 125px;
}
.box:nth-child(2) {
background: lightgreen;
top: 250px;
left: 250px;
}
<div class="draggable box"></div>
<div class="draggable box"></div>
<div class="draggable box"></div>
Objective
试图复制 jQueryUI 的 .draggable
行为。
$(function() {
$(".box").draggable();
});
.box {
width: 100px;
height: 100px;
background: pink;
}
<head>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
</head>
<body>
<div class="box"></div>
</body>
一个不是很多可行的解决方案
我创建了一个函数 draggable
来执行此操作。它接受一个 HTMLElement
类型的对象作为参数:需要拖动的元素。函数需要遵循以下两点:
可以在多个元素上调用该函数,从而可以拖动页面上的多个元素(像jQueryUI那样一次只能拖动一个元素)
可以将可拖动元素拖动到页面上用户希望移动的任何位置(更准确地说是在父容器中)。
我已经开始编写函数了,它的工作原理如下:
它在 元素上添加一个
onmousedown
事件侦听器当事件被触发时,从鼠标位置到被拖动元素角的距离(在
x
和y
上)保存在relativeX
和[=27=内].dragging
的状态也从false
更改为true
。它在
上添加了一个document
onmousemove
事件侦听器当用户的鼠标在页面上移动时,该事件不断被触发。这允许检查
dragging
的状态是否为true
。当状态确实为true
时,表示用户正在拖动元素。因此需要更新元素的位置。考虑到相对位置(之前保存的relativeX
和relativeY
),我使用绝对定位来使这些位置正确。它在
document
上添加onmouseleave
和onmouseup
以在需要时结束行为:当用户停止拖动元素(鼠标左键 向上)或离开
document
时,函数会将dragging
的状态更改回false
。因此onmousemove
监听器将继续监听但不会更新位置,直到dragging
再次设置回true
。
下面是更好说明的代码:
const relativePos = (e, p) => {
let rect = e.target.getBoundingClientRect()
return [e.clientX - rect.x, e.clientY - rect.y];
}
const draggable = draggedE => {
let isDragging, relativeX, relativeY;
// checking if the user is dragging
document.onmousemove = (event) => {
if (isDragging) {
draggedE.style.left = `${event.clientX - relativeX}px`
draggedE.style.top = `${event.clientY - relativeY}px`
}
}
// when the user initiate the dragging behaviour
draggedE.onmousedown = e => {
[isDragging, relativeX, relativeY] = [true, ...relativePos(e)]
}
// listener to end the behaviour
document.onmouseup = () => isDragging = false
document.onmouseleave = () => isDragging = false
}
draggable(document.querySelector('.box'))
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
}
<div class="box"></div>
事件侦听器有问题
代码完美运行。 但这是我的担忧:
我使用了很多可拖动元素。对于它们中的每一个,都有一个事件附加到文档以检查 dragging
的状态。这似乎对性能不太好。
此外,如果您仔细观察下面的动画(使用 jQueryUI 的 .draggable
方法):
您可以看到我可以在不停止行为的情况下离开页面:我可以将元素拖出页面。我发现我只能通过将我的听众附加到 document
来实现这样的结果。将它附加到父元素甚至 body
都不起作用。
我的问题
如何在不使用那么多事件处理程序(在父级上,在 document
上更糟)的情况下实现上述结果,同时仍确保相同的行为?
[编辑] 可能的解决方案
Teemu建议:
我创建了一个包含所有方法的对象
draggable
在
document
上添加唯一的
mousedown
事件处理程序
这是它的样子:
const draggable = {
startDrag: function(event) {
this.element = event.target
const rect = event.target.getBoundingClientRect();
this.offset = [event.clientX - rect.x, event.clientY - rect.y];
},
dragging: function(e) {
this.element.style.left = `${e.clientX - this.offset[0]}px`;
this.element.style.top = `${e.clientY - this.offset[1]}px`;
},
endDrag: function() {},
element: '',
offset: []
};
document.addEventListener('mousedown', e => {
if (e.target.classList.contains('draggable')) {
draggable.startDrag(e)
const _mousemove = draggable.dragging.bind(draggable)
document.addEventListener('mousemove', _mousemove);
document.addEventListener('mouseup', function _mouseup(event) {
document.removeEventListener('mousemove', _mousemove)
document.removeEventListener('mouseup', _mouseup)
});
}
});
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
user-select: none;
}
.box:nth-child(1) {
background: lightblue;
top: 125px;
left: 125px;
}
.box:nth-child(2) {
background: lightgreen;
top: 250px;
left: 250px;
}
<div class="draggable box"></div>
<div class="draggable box"></div>
<div class="draggable box"></div>
必须命名处理程序使用的函数以便以后删除它们让我很烦恼。 有没有其他方法可以在不调用 bind
的情况下删除事件?
感谢 Teemu 我找到了解决问题的方法。这个想法是在用户不拖动时 运行 一个唯一的事件监听器。当他拖动一个元素时,其他事件侦听器将被添加 并在用户结束拖动过程后立即被删除 使用 mouseup
或 mouseleave
.
draggable
是一个包含所有函数的对象:startDrag
、dragging
和 endDrag
以及信息:element
和 offset
关于拖动机制。
为了删除事件侦听器,我需要将我的事件函数保存在变量中。
此外,我必须使用 bind()
才能访问 draggable
在不同函数中的属性。所以我想出了 属性 events
,其中充满了 draggable
的函数绑定到 draggable
...
所以我想出了:
(function() {
this.events = {
_mousemove: this.dragging.bind(this),
_mouseup: this.endDrag.bind(this)
}
}.bind(draggable))()
而不是写作:
const _mousemove = draggable.dragging.bind(draggable)
const _mouseup = draggable.endDrag.bind(draggable)
所以现在我可以通过 this.events._mousemove
和 this.events._mouseup
.[=36 访问链接到 _mousemove
和 _mouseup
内部的事件来停止 endDrag
=]
现在可以添加事件侦听器:
document.addEventListener('mousemove', draggable.events._mousemove);
document.addEventListener('mouseup', draggable.events._mouseup);
这是完整的 JavaScript 代码:
const draggable = {
events: {},
startDrag: function(event) {
this.element = event.target
const rect = event.target.getBoundingClientRect();
this.offset = [event.clientX - rect.x, event.clientY - rect.y];
},
dragging: function(e) {
this.element.style.left = `${e.clientX - this.offset[0]}px`;
this.element.style.top = `${e.clientY - this.offset[1]}px`;
},
endDrag: function() {
document.removeEventListener('mousemove', this.events._mousemove)
document.removeEventListener('mouseup', this.events._mouseup)
},
element: '',
offset: []
};
document.addEventListener('mousedown', e => {
(function() {
this.events = {
_mousemove: this.dragging.bind(this),
_mouseup: this.endDrag.bind(this)
}
}.bind(draggable))()
if (e.target.classList.contains('draggable')) {
draggable.startDrag(e)
document.addEventListener('mousemove', draggable.events._mousemove);
document.addEventListener('mouseup', draggable.events._mouseup);
}
});
.box {
width: 100px;
height: 100px;
background: pink;
position: absolute;
user-select: none;
}
.box:nth-child(1) {
background: lightblue;
top: 125px;
left: 125px;
}
.box:nth-child(2) {
background: lightgreen;
top: 250px;
left: 250px;
}
<div class="draggable box"></div>
<div class="draggable box"></div>
<div class="draggable box"></div>