提到元素拖拽,通常都会先想到用 HTML5 的拖拽放置 (Drag 和 Drop) 来实现,它提供了一套完整的事件机制,看起来似乎是首选的解决方案,但实际却不是那么美好,主要是它的样式太过简陋,无法实现更高级的用户体验:
这是浏览器默认的拖拽效果,点住拖拽任意图片或文字都会产生。
笔者因为之前有个小项目需要经常参考稿定设计,一直有留意其元素拖拽的效果(如下图),所以接下来我将以这种效果为蓝本,使用原生 JS 实现一个富有动感的 自定义拖拽 效果,话不多说直接开摸。
首先说下思路,我们需要知道鼠标的三个事件,分别是 mousedown,mousemove,mouseup ,当点击按下的时候,克隆一个绝对定位的元素,并标识下"拖拽中"的状态,接着在 mousemove 中就可以判断应该执行的具体方法,从而让元素随着鼠标移动起来。
在监听事件的 event 对象中,有几个参数是比较重要的:clientX,clientY 标识的鼠标当前横坐标和纵坐标,offsetX 和 offsetY 表示相对偏移量,可以在 mousedown 鼠标按下时记录初始坐标,在 mouseup 鼠标抬起时判断是否在目标区域中,如果是则用鼠标获取到的当前的偏移量 - 初始坐标得到元素实际在目标区域中的位置。
为了阅读体验,以下所有代码均有部分省略,文末可查看完整源码地址,代码量并不多。
先简单实现一个两栏布局界面,并应用上一些 CSS 效果:
<div id="app">
<div class="slide">
<div id="list">
<img class="item" src="......." />
<img .........
</div>
</div>
<div class="content"></div>
</div>
#app {
width: 100vw;
height: 100vh;
display: flex;
}
.active {
cursor: grabbing;
}
.slide {
width: 260px;
height: 100%;
overflow: scroll;
border-right: 1px solid rgba(0,0,0,.15);
#list {
user-select: none;
.item {
background: rgba(0,0,0,.15);
width: 120px;
display: inline-block;
break-inside: avoid;
margin-bottom: 4px;
}
.item:hover {
cursor: grab;
filter: brightness(90%);
}
.item:active {
cursor: grabbing;
}
}
.grid {
column-count: 2;
column-gap: 0px;
}
}
.slide::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
#content {
position: relative;
flex: 1;
height: 100%;
margin-left: 45px;
background: rgba(0,0,0,.07);
.item {
position: absolute;
transform-origin: top left;
}
}
利用滤镜 filter: brightness(90%); 调节明亮度可以快速实现一个鼠标覆盖的动态效果,无需额外制作遮罩:
使用伪类激活 cursor 的 grab 和 grabbing 可以设置抓取动作的图标:
利用事件委托机制为选择列表添加 mousedown 事件监听,实现抓取的原理是在鼠标按下时克隆按下的元素,并把克隆出来的元素设置成绝对定位,让它"浮"起来:
let dragging = false
let cloneEl = null // 克隆元素
let initial = {} // 初始化数据记录
......
// 选中了元素
cloneEl = e.target.cloneNode(true) // 克隆元素
cloneEl.classList.add('flutter') // 使其浮动
e.target.parentElement.appendChild(cloneEl) // 加入到列表中
dragging = true // 标记拖动开始
// TODO: 初始化克隆元素的定位并记录,方便后面移动时计算位置
........
.flutter {
position: absolute;
z-index: 9999;
pointer-events: none;
}
将鼠标的坐标设置为克隆元素的绝对定位值(left、top),就会像下图所示这样,此时减去 offset 偏移量,就能让克隆元素覆盖在本体上面。
初始化的值需要记录起来方便后续计算,同时我们用 dragging 变量标记了状态(拖动中),接下来配合移动鼠标的监听事件就能将元素“抓”起来了:
// 鼠标移动
window.addEventListener("mousemove", (e) => {
if (dragging && cloneEl) {
// TODO: 处理元素的移动:改变 left top 定位
// x 轴(left)计算方法:e.clientX - initial.offsetX
// y 轴(top)计算方法:e.clientY - initial.offsetY
}
})
上面只是实现了元素的拖动,但是"克隆"的效果实在太明显了,为了让元素看起来更像是拖出来的而不是复制出来的,我们还要让本体隐藏,同时DOM结构不能丢失,这时只需在按下拖动时给本体元素设置个 opacity: 0,结束时再改回透明度1就能搞定。
虽然到这功能就算实现了,但实际效果还是有点僵硬,参考稿定设计中的元素放开时会固定回到一个位置,然后再收回去,这个过渡又有点鬼畜,不够流畅。其实只需让元素回退过程有一个自然地动画就行,transition 就能实现:
.is_return {
transition: all 0.3s;
}
// 鼠标抬起
window.addEventListener("mouseup", (e) => {
dragging = false
if (cloneEl) {
cloneEl.classList.add('is_return') // 加上过渡动画
changeStyle(......) // 设置回元素的初始位置
setTimeout(() => {
cloneEl.remove() // 移除元素
}, 300)
}
})
最终我在动作结束时给克隆元素添加了过渡属性,然后直接设置回初始坐标让克隆元素回到它的出生地点,用定时器在过渡动画持续的相同时间后移除克隆元素,这样就有了一个平滑稳定的回退动画。
由于在改变元素状态的过程中需要频繁进行多个 CSS 操作,为降低回流重绘的成本,最好将多个操作合并起来处理,这里利用了 cssText 来实现:
// 改变漂浮元素:x、y、缩放倍率
function moveFlutter(x, y, d = 0) {
const scale = d ? initial.width + d < initial.fakeSize ? `transform: scale(${(initial.width + d) / initial.width})` : null : null
const options = [`left: ${x}px`, `top: ${y}px`]
scale && options.push(scale)
// 将CSS处理成数组,然后丢进DOM操作方法中一次执行
changeStyle(options)
}
// 合并多个操作
function changeStyle(arr) {
const original = cloneEl.style.cssText.split(';')
original.pop()
cloneEl.style.cssText = original.concat(arr).join(';') + ';'
}
放大我们可以使用 transform: scale 来实现,只需要将拖动位置之间的距离当做变化系数(假设为d),那么scale变化数值即为(元素宽度 + d)/元素宽度,而放大的最终倍数必定为 图片实际宽度/元素的宽度,只要判断不超过这个边界就可以。(这个图片实际宽高在真实业务场景中建议在上传资源时就记录在数据库,这里我是模拟的随机一个原图尺寸)。
两点间距离计算公式为:
代码实现:
// 计算两点之间距离
function distance({ clientX, clientY }) {
const { clientX: x, clientY: y } = initial // 获取初始的坐标
const b = clientX - x;
const a = clientY - y;
return Math.sqrt(Math.pow(b, 2) + Math.pow(a, 2))
}
window.addEventListener("mousemove", (e) => {
if (dragging && cloneEl) {
const d = distance(e) // 计算距离
moveFlutter(e.clientX - initial.offsetX, e.clientY - initial.offsetY, d)
}
})
function moveFlutter(x, y, d = 0) {
let scale = ''
// 如果距离大于0,且宽度+距离小于实际宽度
if( d && initial.width + d <= initial.fakeSize ) {
scale = `transform: scale(${(initial.width + d) / initial.width})`
}
// TODO ... changeStyle ...
}
效果演示:
注意元素都要设置 transform-origin: top left; 改变缩放原点到左上角,否则默认(中心为原点)的转换会发生比较明显的偏移。
其实拖拽放置有点像是"复制"与"粘贴",前面我们实现了复制,放置主要就是将元素粘贴到画布当中,流程步骤如下:
// 完成处理
function done(x, y) {
if (!cloneEl) { return }
const newEl = cloneEl.cloneNode(true)
newEl.classList.remove('flutter')
newEl.src = cloneEl.getAttribute('raw') // 设置原图地址
newEl.style.cssText = `left: ${x - initial.offsetX}px; top: ${y - initial.offsetY}px;`
document.getElementById('content').appendChild(newEl)
// TODO: 元素移除
}
判断是否在画布内抬起很简单,往画布上绑定mouseup监听事件即可,克隆的新元素必须删除无用的属性和class,此时设置元素的left、top即可将元素放置进画布中,关键点在于画布内的target有可能是错的,因为如果鼠标抬起的区域已经放置了元素,那么相对偏移量就得我们自己计算了,使用getBoundingClientRect方法获取画布本身相对于视窗的偏移,鼠标坐标减去画布本身的偏移就是元素在画布中的位置了。
document.getElementById('content').addEventListener("mouseup", (e) => {
if (e.target.id !== 'content') {
const lostX = e.x - document.getElementById('content').getBoundingClientRect().left
const lostY = e.y - document.getElementById('content').getBoundingClientRect().top
done(lostX, lostY)
} else { done(e.offsetX, e.offsetY) }
})
只贴了部分关键代码,完整代码文末查看。
如果不对边界情况进行处理可能会导致拖动时发生意外的中断,无法正确回收克隆元素。
// 鼠标离开了视窗
document.addEventListener("mouseleave", (e) => {
end()
})
// 用户可能离开了浏览器
window.onblur = () => {
end()
}
参考稿定设计中元素拖拽是直接赋值原图的,原图大小通常无法控制,免不了需要加载时间,造成卡顿空白的问题,在网络不够快时体验尤其尴尬:
我的优化思路是利用浏览器加载过同一张图片就会优先读缓存的机制,先用一个Image加载原图,等其加载完毕再把拖拽元素的src改成原图,这样浏览器会"自动"帮我们优化这个过程,只需要注意一点,由于这是个异步任务,所以一定要做好对应标记,不然手速快的时候控制不好触发顺序。
function simulate(url, flag) {
cloneEl.setAttribute('raw', url)
const image = new Image()
image.src = url
image.onload = function () {
// 异步任务,克隆节点可能已不存在,flag标记是否拖动的还是当前目标
cloneEl && initial.flag === flag && (cloneEl.src = url)
}
}
效果演示,故意加大了图片的分辨率差异:
以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注,我会更新更多实用的前端知识与技巧,我是茶无味的一天,期待与你共同成长~
[1] 完整代码地址: https://juejin.cn/post/7145447742515445791/#heading-9
[2] 关于作者: https://book.palxp.com
CSDN 编者按】Wasm 是否会取代 JavaScript ?这是 WebAssembly 发布之初不少人发出的疑问,本文作者在通过各项基准测试之后,回答了这个问题。
链接:https://thenewstack.io/javascript-vs-wasm-which-is-more-energy-efficient-and-faster/
JavaScript 还是 WebAssembly(简称 Wasm),究竟哪个运行速度更快、更节能?葡萄牙米尼奥大学对这个问题展开了研究,并得出了结论:虽然在实验室微基准测试方面,JavaScript 比 Wasm 更节能、更快,但在实际应用程序中,Wasm 在速度和节能方面皆优于 JavaScript,有时能高出 30%。
请不要忘记,如今 Wasm 还处于发展的早期阶段。
研究员兼软件工程师 João De Macedo 表示:“Wasm 仍处于起步阶段,只有时间能告诉我们它将如何发展。在我们看来,Wasm 完全有可能战胜原生应用,并帮助网络浏览器成为 21 世纪的操作系统。”
该研究于 2022 年发表,不仅参考了微观基准,也考虑了实际情况。
João De Macedo解释道:“微基准测试是一种程序,用于跟踪和测量某个明确定义的任务的性能,例如持续时长、操作速率、带宽等。微基准测试是测量软件系统性能的主要方法之一,因此,Wasm 也不例外。”
由于 Wasm 的主要目标之一是提高 Web 应用程序的性能,因此比较 Wasm 和 JS 的运行时和节能的表现非常重要。
从微基准测试来看,在有些情况下,JavaScript 在速度和节能方面的表现都超过了 Wasm。然而,在 Google Chrome 和微软 Edge 上,Wasm 不仅比 JavaScript 更节能,而且性能也更好。但是,JavaScript 在 Mozilla Firefox 上确实比 Wasm 具有更好的性能,而且大多数时候的差异很明显。
尽管如此,最终 Wasm 仍将在实际的应用程序中占据主导地位。
报告称,“初步结果表明,WebAssembly 虽然仍处于起步阶段,但已开始超越 JavaScript,并且 WebAssembly 的成长空间也更大。统计分析表明,与 JavaScript 相比,WebAssembly 表现出了显著的性能差异。”
该研究通过 Wasmboy 基准测试,测量了 Wasm 和 JavaScript 在实际应用程序中的表现。Wasmboy 基准测试是一个 Gameboy/Gameboy Color 模拟器,是用Typescript 编写的Wasm基准测试。Wasmboy 是用 JavaScript/TypeScript 编写的,创建的主要目标是比较 AssemblyScript 编译器生成的 Wasm 与 TypeScript 编译器生成的 ES6 最新版 JavaScript 之间的运行时性能。
报告称:“该游戏机包括六个开源游戏,可以从游戏机中运行。我们更新了 WasmBoy 的源代码,指定了执行游戏的浏览器。”
因此,总共有六款游戏在三种浏览器(Chrome、Edge 和 Firefox)上运行,使用两种语言,这样团队就有了 36 个独特的样本。
此外,他们还使用了 PSPDFKit 基准测试。该基准测试使用的软件支持在任何平台上查看、注释和填写 PDF文档中的表格。该报告指出,创建开源基准是为了评估将软件移植到 Wasm 生态系统的可能性,并比较 Wasm 与 JavaScript 的实现。该团队修改了应用程序的源代码,使用这两种语言(was 和 asm.js)执行多个输入。为了利用实际输入执行基准测试,该团队考虑了五个不同的 pdf 文档,其中包括将一本书分为三个部分、一篇科学论文和 20 张幻灯片。
报告称,“与 Wasmboy 基准测试类似,我们编写了一些 makefile,在不同浏览器中自动执行测试”,结果得到了在三种浏览器中运行的、用两种语言编写的五个示例程序,也就是说共有 30 个各不相同的程序。
此外,此次研究还考虑了各种微基准,这些程序最初是用 C 编写的,然后使用 Emscripten 编译器编译成了 Wasm 和 JavaScript。还有一些其他的语言也可编译为 Wasm,其中包括 C/C++、Rust、Go、Python 和 AssemblyScript(TypeScript 的一种形式)。
有关微基准测试和研究其他方面的详细信息,请参见 João De Macedo、Rui Abreu、Rui Pereira 和 João Saraiva 的论文《WebAssembly与JavaScript:能源和运行时性能》(https://ieeexplore.ieee.org/document/9830108)。
总的来说,他们可以通过这种方法检查JS 和 Wasm 如何以不同方式处理规模和输入大小。之前有研究使用了这种方法,但只检查了虚拟机的性能。De Macedo 的研究希望了解真实世界的应用程序,因此该团队开发了一个框架来测量基于浏览器的环境中的性能。
De Macedo 认为,“也许永远不会,因为 JS 更适合不需要超高性能的网页。目前,Wasm 只能作为 JS 的补充,而不能取而代之。但是,如果 Wasm 得到进一步发展,就有可能在某些应用程序中取代 JS,因为 Wasm 的加载时间更快,而且资源的使用效率更高。”
De Macedo 认为,从长远来看 Wasm 将带来颠覆性的改变。
他表示:“Wasm 不仅会彻底改变Web,而且还有可能颠覆技术市场的多个领域,包括云,尽管越来越多的组织采用了容器模式,但并未能真正满足每个人都需求。”
:如果你不知道王垠(垠神)是谁,可以先搜一下。
很多JavaScript程序员也盲目地鄙视Java,而其实JavaScript比Python和Ruby还要差。不但具有它们的几乎所有缺点,而且缺乏一些必要的设施。JavaScript的各种“WEB框架”,层出不穷,似乎一直在推陈出新,而其实呢,全都是在黑暗里瞎蒙乱撞。JavaScript的社区以幼稚和愚昧著称。你经常发现一些非常基本的常识,被JavaScript“专家”们当成了不起的发现似的,在大会上宣讲。我看不出来JavaScript社区开那些会议,到底有什么意义,仿佛只是为了拉关系找工作。
Python凑合可以用在不重要的地方,Ruby是垃圾,JavaScript是垃圾中的垃圾。原因很简单,因为Ruby和JavaScript的设计者,其实都是一知半解的民科。然而世界就是这么奇怪,一个彻底的垃圾语言,仍然可以宣称是“程序员最好的朋友”,从而得到某些人的爱戴……
面向对象语言不仅有自身的根本性错误,而且由于面向对象语言的设计者们常常是半路出家,没有受到过严格的语言理论和设计训练却又自命不凡,所以经常搞出另外一些奇葩的东西。比如在JavaScript里面,每个函数同时又可以作为构造函数(constructor),所以每个函数里面都隐含了一个this变量,你嵌套多层对象和函数的时候就发现没法访问外层的this,非得bind一下。Python的变量定义和赋值不分,所以你需要访问全局变量的时候得用global关键字,后来又发现如果要访问“中间层”的变量,没有办法了,所以又加了个nonlocal关键字。Ruby先后出现过四种类似lambda的东西,每个都有自己的怪癖…… 有些人问我为什么有些语言设计成那个样子,我只能说,很多语言设计者其实根本不知道自己在干什么!
在 C 这样的语言里,由于结构上有很多限制,所以才觉得那样的语法还可以。可是一旦加入 Lisp 的那些表达能力强的结构,就发现越来越难看。JavaScript(node.js)就是对此最好的一个证据。
如果你了解一点历史就会发现,今天非常流行的 JavaScript,其实不过是一个“没能正确实现的 Scheme”。
合理的入门语言
所以初学者要想事半功倍,就应该从一种“合理”的,没有明显严重问题的语言出发,掌握最关键的语言特性,然后由此把这些概念应用到其它语言。哪些是合理的入门语言呢?我个人觉得这些语言都可以用来入门:Scheme、C、Java、Python、JavaScript
那么相比之下,我不推荐用哪些语言入门呢?Shell、PowerShell、AWK、Perl、PHP、Basic、Go、Rust
一进门就感觉这跟一般的 meetup 气氛很不一样。这大周末晚上的,清一色的爷们,没有一个女人,也没有笑声。而且里面的人说话都很奇怪,不正眼看人,有些好像怒目相向的样子,说出话来就像在查你户口。有几次有人问我是干什么的,我刚一开口,他们一句话不回,扭头就跟其他人说话去了。只有一个头发花白的大叔工程师对我挺友好的,于是我们就聊起来。旁边有个华人工程师盯着一个15寸的 Macbook,后来也聊起来,开门见山就问我用什么语言。我也忘了我说什么了,只记得他很自豪的说自己用 JavaScript,而且那是最高配置的 Macbook,是 Retina 显示器的。
今天我来谈一下另外一种错误的倾向,这种倾向也导致了很多错误,并且继续在导致错误的产生。
今天我要说的错误倾向叫做“试图容纳世界”。这个错误导致了 Python,Ruby 和 JavaScript 等“动态语言”里面的一系列问题。
认识和承认计算机系统里的历史遗留糟粕
很多不尊重人现象的起源,都是因为某些人偏执的相信某种技术就是世界上最好的,每个人都必须知道,否则他就不是一个合格的程序员。这种现象在Unix(Linux)的世界尤为普遍。Unix系统的鼓吹者们(我曾经是其中之一)喜欢到处布道,告诉你其它系统的设计有多蠢,你应该遵从Unix的“哲学”。他们仿佛认为Unix就是世界终极的操作系统,然而事实却是,Unix是一个设计非常糟糕的系统。它似乎故意被设计为难学难用,容易犯错,却美其名曰“强大”,“灵活”。眼界开阔一点的程序员都知道,Unix的设计者其实基本不懂设计,他们并不是世界上最好的程序员,却有一点做得很成功,那就是他们很会制造宗教,煽动人们的盲从心理。Unix设计者把自己的设计失误推在用户身上,让用户觉得学不会或者搞错了都是自己的错。
如果你对计算机科学理解到一定程度,就会发现我们其实仍然生活在计算机的石器时代。特别是软件系统,建立在一堆历史遗留的糟糕设计之上。各种蹩脚脑残的操作系统(比如Unix,Linux),程序语言(比如C++,JavaScript,PHP,Go),数据库,编辑器,版本控制工具,…… 时常困扰着我们,这就是为什么你需要那么多的所谓“经验”和“知识”。然而,很多IT公司不喜欢承认这一点,他们一向以来的作风是“一切都是程序员的错!”,“作为程序员,你应该知道这些!” 这就造成了一种“皇帝的新装现象”——大家都不喜欢用一些设计恶劣的工具,却都怕别人嘲笑或者怀疑自己的能力,所以总是喜欢显示自己“会用”,“能学”,而没有人敢说它难用,敢指出设计者的失误。
我看完之后的感觉:
说得真TM对,吐槽都吐到点子上了。JS 这么垃圾,为什么垠神还推荐入门用 JS 呢。可能有些语言比 JS 还垃圾吧,哈哈。虽然观点上有矛盾,但是牛逼的人的大脑都是能容忍矛盾的,问题不大。
为什么我要发这篇文章:
如果你不能接受 JS 的缺点,说明你不爱 JS。
*请认真填写需求信息,我们会在24小时内与您取得联系。