最小化事件侦听器的数量以实现 "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 类型的对象作为参数:需要拖动的元素。函数需要遵循以下两点:

我已经开始编写函数了,它的工作原理如下:

  1. 它在 元素上添加一个 onmousedown 事件侦听器

    当事件被触发时,从鼠标位置到被拖动元素角的距离(在xy上)保存在relativeX和[=27=内]. dragging 的状态也从 false 更改为 true

  2. 它在 document

    上添加了一个 onmousemove 事件侦听器

    当用户的鼠标在页面上移动时,该事件不断被触发。这允许检查 dragging 的状态是否为 true。当状态确实为 true 时,表示用户正在拖动元素。因此需要更新元素的位置。考虑到相对位置(之前保存的 relativeXrelativeY),我使用绝对定位来使这些位置正确。

  3. 它在 document 上添加 onmouseleaveonmouseup 以在需要时结束行为:

    当用户停止拖动元素(鼠标左键 向上)或离开 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建议:

这是它的样子:

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 我找到了解决问题的方法。这个想法是在用户不拖动时 运行 一个唯一的事件监听器。当他拖动一个元素时,其他事件侦听器将被添加 并在用户结束拖动过程后立即被删除 使用 mouseupmouseleave.

draggable 是一个包含所有函数的对象:startDragdraggingendDrag 以及信息:elementoffset 关于拖动机制。

为了删除事件侦听器,我需要将我的事件函数保存在变量中。

此外,我必须使用 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._mousemovethis.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>