keras 香草编码器
Drag-and-drop functionality is the bread and butter of a modern web UX. It’s an aspect of the API, part of the HTML standard. Lots of UI libraries provide it out of box.
拖放功能是现代Web UX的基础。 这是API的一个方面 ,是HTML标准的一部分。 许多UI库都是开箱即用的。
Sometimes, though, we have to deal with situations where neither the standard API nor the libraries can help, and we have to implement the functionality ourselves. In this article we’ll do exactly that—we’ll implement:
但是,有时候,我们必须处理标准API和库都无法提供帮助的情况,而我们必须自己实现功能。 在本文中,我们将完全做到这一点-我们将实现:
- Cross-input (with blended mouse and touch-events support) 交叉输入(具有混合的鼠标和触摸事件支持)
- Cross-browser friendly 跨浏览器友好
- Performant (60 FPS) 性能(60 FPS)
- Efficient (no wasted JavaScript processing) 高效(不浪费JavaScript处理)
1.先决条件 (1. Prerequisites)
For the sake of this article, we’re going to be dragging a few black square boxes (divs) around a container (div) element in a simple HTML page:
为了本文的目的,我们将在一个简单HTML页面中的容器(div)元素周围拖动几个黑色方框(divs):
<div class="container">
<div style="top: 100px; left: 100px" class="box"></div>
<div style="top: 200px; left: 200px" class="box"></div>
<div style="top: 300px; left: 300px" class="box"></div>
<div style="top: 400px; left: 400px" class="box"></div>
</div>
.container {
width: 600px;
height: 600px;
background-color: darkgrey;
touch-action: none;
}
.box {
position: absolute;
width: 100px;
height: 100px;
background-color: black;
}
Nothing fancy here, but note we’re applying the touch-action: none;
CSS rule to prevent any browser level manipulations (such as scroll or zoom) for events inside the container.
这里没什么好看的,但请注意,我们正在应用touch-action: none;
CSS规则 ,可防止对容器内的事件进行任何浏览器级别的操作(例如,滚动或缩放)。
2.添加输入监听器 (2. Adding Input Listeners)

Since we want to support cross-input, we could write some detection mechanism for whether the user is using touch input or mouse input, but in the modern world, the user might be using both input types at the same time (touchscreen laptop, anyone?).
因为我们要支持交叉输入,所以我们可以编写一些检测机制来确定用户是使用触摸输入还是鼠标输入,但是在现代世界中,用户可能同时使用两种输入类型 (触摸屏笔记本电脑,有人吗?)。
Hence, we should aim to support both. One way would be to attach both types of listeners, but a better way is to leverage the Pointer Events API — the blender of two input types that nowadays has decent support by all of the major browsers.
因此,我们应该努力支持两者。 一种方法是连接两种类型的侦听器,但更好的方法是利用Pointer Events API-两种输入类型的混合器,如今,所有主要浏览器都提供了不错的支持。
Our drag-and-drop user experience can be mapped this way:
可以通过以下方式映射我们的拖放式用户体验:
User points at a box, presses the mouse button/screen (
pointerdown
)用户指向一个框,按下鼠标按钮/屏幕(
pointerdown
)User drags the box by moving the mouse/their finger across the screen (
pointermove
)用户通过在屏幕上移动鼠标/手指来拖动框(
pointermove
)User drops the box by releasing the mouse/their finger from the screen (
pointerup
/pointercancel
*)用户通过从屏幕上释放鼠标/手指来
pointerup
pointercancel
(pointerup
/pointercancel
*)
*Touch devices feature an additional touchcancel
event — for the cases when the touch event gets disrupted (for example, when there are too many touch points on the screen).
*触摸设备还具有一个附加的touchcancel
事件-在触摸事件中断的情况下(例如,当屏幕上的触摸点过多时)。
So let’s write some JavaScript and add our event listeners:
因此,让我们编写一些JavaScript并添加事件监听器:
const container = document.querySelector('.container');
container.addEventListener('pointerdown', userPressed, { passive: true });
function userPressed(event) {
element = event.target;
if (element.classList.contains('box')) {
console.log("pick up!")
container.addEventListener('pointermove', userMoved, { passive: true });
container.addEventListener('pointerup', userReleased, { passive: true });
container.addEventListener('pointercancel', userReleased, { passive: true });
};
};
function userMoved(event) {
console.log("dragging!")
};
function userReleased(event) {
console.log("dropped!")
container.removeEventListener('pointermove', userMoved);
container.removeEventListener('pointerup', userReleased);
container.removeEventListener('pointercancel', userReleased);
};
一些注意事项 (A few notes)
We’re only attaching the
pointerdown
event to the container and adding/removing other listeners dynamically to prevent event pollution — we’re using listeners on an as-needed basis, keeping things tight.我们只是将
pointerdown
事件附加到容器上,并动态添加/删除其他侦听器以防止事件污染—我们在需要时使用侦听器,以保持紧密状态。The events are attached to the container, as we’re using event delegation.
事件被附加到容器,因为我们正在使用事件委托 。
We’re also using the
passive
flag to ensure our events don’t interfere with our scrolling events.我们还使用
passive
标志来确保事件不会干扰滚动事件。
3.使盒子移动 (3. Making Boxes Move)
Our boxes feature absolute
positioning and rely on left
/top
properties to be positioned inside of the container. Let’s add functionality to actually move the boxes around:
我们的盒子具有absolute
定位功能,并依靠left
/ top
属性在容器内部定位。 让我们添加功能以实际移动框:
const container = document.querySelector('.container');
container.addEventListener('pointerdown', userPressed, { passive: true });
var element, bbox, startX, startY;
function userPressed(event) {
element = event.target;
if (element.classList.contains('box')) {
startX = event.clientX;
startY = event.clientY;
bbox = element.getBoundingClientRect();
container.addEventListener('pointermove', userMoved, { passive: true });
container.addEventListener('pointerup', userReleased, { passive: true });
container.addEventListener('pointercancel', userReleased, { passive: true });
};
};
function userMoved(event) {
let deltaX = event.clientX - startX;
let deltaY = event.clientY - startY;
element.style.left = bbox.left + deltaX + "px";
element.style.top = bbox.top + deltaY + "px";
};
function userReleased(event) {
container.removeEventListener('pointermove', userMoved);
container.removeEventListener('pointerup', userReleased);
container.removeEventListener('pointercancel', userReleased);
};
There are now more things going on here:
现在,这里还有更多事情要做:
Inside the
userPressed
event handler, we capture the coordinates of the starting point for the movement as well as the box element’sBoundingClientRect
; both will help to calculate the amount of movement we need to apply to the box based on the pointer’s distance covered during the move.在
userPressed
事件处理程序内部,我们捕获了运动起点的坐标以及box元素的BoundingClientRect
; 两者都将有助于根据移动过程中指针所经过的距离来计算我们需要应用于框的移动量。Inside the
userMoved
handler, we leveragestartX
andstartY
data to calculate thedelta
for which the box should visually move in relation to its previous position.在
userMoved
处理程序内部,我们利用startX
和startY
数据来计算框应相对于其先前位置在视觉上移动的delta
。
Now the user can, in fact, move the boxes and do so using touch or mouse input.
现在,用户实际上可以移动盒子并使用触摸或鼠标输入进行移动。
4.调优性能 (4. Tuning Performance)
There are a few known approaches we can take to improve performance. In this simplistic case, we don’t perform any heavy calculations; hence, even as is the performance looks fine. But we can consider the following options:
我们可以采取几种已知的方法来提高性能。 在这种简单的情况下,我们不执行任何繁重的计算; 因此,即使性能看起来也不错。 但是我们可以考虑以下选择:
'transform3d' (‘transform3d’)
This trick might be out of date nowadays, as modern browsers learned to optimize for layout changes, but it’s still useful to know that instead of directly modifying our box’s left
and top
positions, we could apply transform
during pointermove
and only apply actual style changes on pointerup
:
由于现代浏览器已学会优化布局更改,因此这种技巧可能已不合时宜,但了解而不是直接修改框的left
和top
位置,我们可以在pointermove
期间应用transform
并仅对pointerup
:
const container = document.querySelector('.container');
container.addEventListener('pointerdown', userPressed, { passive: true });
var element, bbox, startX, startY, deltaX, deltaY;
function userPressed(event) {
element = event.target;
if (element.classList.contains('box')) {
startX = event.clientX;
startY = event.clientY;
bbox = element.getBoundingClientRect();
container.addEventListener('pointermove', userMoved, { passive: true });
container.addEventListener('pointerup', userReleased, { passive: true });
container.addEventListener('pointercancel', userReleased, { passive: true });
};
};
function userMoved(event) {
deltaX = event.clientX - startX;
deltaY = event.clientY - startY;
element.style.transform = "translate3d("+deltaX+"px,"+deltaY+"px, 0px)";
};
function userReleased(event) {
container.removeEventListener('pointermove', userMoved);
container.removeEventListener('pointerup', userReleased);
container.removeEventListener('pointercancel', userReleased);
element.style.left = bbox.left + deltaX + "px";
element.style.top = bbox.top + deltaY + "px";
element.style.transform = "translate3d(0px,0px,0px)";
};
'RequestAnimationFrame' (‘RequestAnimationFrame’)
We can also achieve smoother animation while dragging (~60 FPS) through the use of the requestAnimationFrame API:
通过使用requestAnimationFrame API,我们还可以在拖动(〜60 FPS)的同时实现更流畅的动画:
const container = document.querySelector('.container');
container.addEventListener('pointerdown', userPressed, { passive: true });
var element, bbox, startX, startY, deltaX, deltaY, raf;
function userPressed(event) {
element = event.target;
if (element.classList.contains('box')) {
startX = event.clientX;
startY = event.clientY;
bbox = element.getBoundingClientRect();
container.addEventListener('pointermove', userMoved, { passive: true });
container.addEventListener('pointerup', userReleased, { passive: true });
container.addEventListener('pointercancel', userReleased, { passive: true });
};
};
function userMoved(event) {
// if no previous request for animation frame - we allow js to proccess 'move' event:
if (!raf) {
deltaX = event.clientX - startX;
deltaY = event.clientY - startY;
raf = requestAnimationFrame(userMovedRaf);
}
};
function userMovedRaf() {
element.style.transform = "translate3d("+deltaX+"px,"+deltaY+"px, 0px)";
// once the paint job is done we 'release' animation frame variable to allow next paint job:
raf = null;
};
function userReleased(event) {
container.removeEventListener('pointermove', userMoved);
container.removeEventListener('pointerup', userReleased);
container.removeEventListener('pointercancel', userReleased);
// if animation frame was scheduled but the user already stopped interaction - we cancel the scheduled frame:
if (raf) {
cancelAnimationFrame(raf);
raf = null;
};
element.style.left = bbox.left + deltaX + "px";
element.style.top = bbox.top + deltaY + "px";
element.style.transform = "translate3d(0px,0px,0px)";
};
这里有几点注意事项 (A few notes here)
We added the
raf
variable to helps us debouncepointermove
events with help from therequestAnimationFrame
timer, as we don’t want JavaScript to perform calculations that won’t be used since the animation frame was already scheduled and hasn’t yet completed.我们添加了
raf
变量,以帮助我们在requestAnimationFrame
计时器的帮助下对pointermove
事件进行反跳,因为我们不希望JavaScript执行由于已经安排好动画框架且尚未完成而不会使用的计算。We made sure the
pointermove
event, which fires many times per second, is lightweight and only contains the logic necessary to obtain the coordinates information and triggers the style change.我们确保了
pointermove
触发多次的pointermove
事件是轻量级的,并且仅包含获取坐标信息并触发样式更改所需的逻辑。
3. CSS规则:“将改变”和“包含” (3. CSS Rules: ‘will-change’ and ‘contain’)
In addition to the above, there are CSS rules that you could explore to tune performance further.
除了上述内容,您还可以探索CSS规则来进一步调整性能。
The will-change
CSS rule indicates to your browser the element’s attributes are expected to change and allows the browser to perform additional optimizations. But as the documentation says — use it with caution:
的 will-change
CSS规则指示浏览器元素的属性都有望改变,并允许浏览器执行额外的优化。 但正如文档所述,请谨慎使用:
“Important:
will-change
is intended to be used as a last resort, in order to try to deal with existing performance problems. It should not be used to anticipate performance problems.”“重要:
will-change
是不得已的手段,目的是试图解决现有的绩效问题。 它不应该被用来预测性能问题。”
.box {
position: absolute;
width: 100px;
height: 100px;
background-color: black;
will-change: transform, left, top;
contain: layout;
}
The contain
CSS rule isn’t fully supported by all browsers yet but can help indicate that a particular DOM’s element style or layout changes won’t affect another page’s elements and thus won’t trigger potential expensive reflows.
在 contain
CSS规则没有完全被所有的浏览器,但还可以帮助支持表示一个特定的DOM的元素风格或布局的变化不会影响其他页面的元素,因而不会触发潜在的昂贵的回流。
It isn’t really adding any value in our simplistic case (given that our boxes are positioned as absolute
, but it’s a useful trick in more complex UI and dragging situations. You can watch this amazing video for more details.
在我们的简单案例中,它并没有真正增加任何价值(假设我们的盒子定位为absolute
,但这在更复杂的UI和拖动情况下是一个有用的技巧。您可以观看此精彩视频 ,了解更多详细信息。
结论 (Conclusion)
We went through the basics of how drag-and-drop functionality can be implemented in pure JavaScript, and we outlined a few techniques and directions for performance optimization. The main things to remember:
我们介绍了如何在纯JavaScript中实现拖放功能的基础知识,并概述了一些性能优化的技术和方向。 要记住的主要事项:
- Leverage blended input events to ensure hybrid devices are covered. 利用混合输入事件来确保涵盖混合设备。
Attach/detach
move
/up
/cancel
events dynamically.动态附加/分离
move
/up
/cancel
事件。Do as much of the heavy weight lifting upfront or inside the
down
andup
handlers, but keep themove
handler lightweight.做尽可能多的重型举重前期的或内部
down
和up
处理程序,但保持move
处理器轻巧。Leverage
requestAnimationFrame
for debouncing and smooth animation while dragging.利用
requestAnimationFrame
在拖动时进行反跳和平滑动画处理。- Explore CSS rules that can help gain additional performance. 探索有助于提高性能CSS规则。
Full implementation here:
完整的实现在这里:
Thank you for reading the article, and happy coding!
感谢您阅读本文,并祝您编程愉快!
keras 香草编码器