一)前言
元素拖动可以用常规的left\top定位属性实现,但是也可以使用css3的位移属性实现,且用transform实现相对于原本的left/top定位方式更加的节省性能,因为left/top会不断的引起重绘、回流与重组,而transform是在单独的图层中进行平移,最后再合并图层,所以不会引起页面单独的回流,且translate3d更有gpu加速,所以性能更优。
(二) 原因探究
可以利用谷歌的调试工具具体看下二者实现方式的不同:
1、比较
transform: translate
position:left/top
由上图的区别可以明显对比出,position在移动过程中会不断的造成布局的重新渲染,而transform只是有重绘的过程;利用传统的position会导致布局改变过多(reflow)导致性能低下,谷歌也给我们报了Cumulative Layout Shifts can result in poor user experiences,使用position会影响用户体验感,会感受到卡顿;
2、知识补充
主线程和合成线程
现代浏览器绘制由多个线程共同作用,但主要由主线程和合成线程进行绘制合并工作:
主线程需要做的任务如下:
- 运行Javascript
- 计算HTML元素的CSS样式
- layout (relayout)
- 将页面元素绘制成一张或多张位图
- 将位图发送给合成线程
合成线程主要任务是:
- 利用GPU将位图绘制到屏幕上
- 让主线程将可见的或即将可见的位图发给自己
- 计算哪部分页面是可见的
- 计算哪部分页面是即将可见的(当你的滚动页面的时候)
- 在你滚动时移动部分页面
在很长的一段时间内,主线程都在忙于运行Javascript和绘制元素。
例如,当用户滚动一个页面时,合成线程会让主线程提供最新的可见部分的页面位图。然而主线程不能及时的响应。这时合成线程不会等待,它会绘制已有的页面位图。对于没有的部分则绘制白屏。
可以使用GPU加速的CSS3属性 CSS transform CSS opacity CSS filter
GPU擅长的领域:
- 绘制东西到屏幕上
- 一次次绘制同一张位图到屏幕上
- 绘制同一张位图到不同的位置、旋转角度和缩放比例
当然还有栅格化等这里就不细究了....
3、结论
所以验证比较之后,通过transform较position去实现元素的拖拽在性能方面确实有更优的性能,能够大幅度减少页面的回流;且transform有浏览器专属的GPU加速,可谓是在性能方面得天独厚;
此外
使用transform属性可以利用GPU 3d加速,只需要给移动的元素加上
transform: translate3d(0, 0, 1px);
will-change: transform;
-webkit-will-change: transform;
即可强制浏览器使用GPU加速,从而获得更加流畅的体验,判断浏览器是否启用了GPU加速,可以定位到该元素,查看元素的计算样式:transform的值是matrix还是matrix3d,matrix3d表示已开启GPU加速;
(三)实现
这里展示实现的关键代码,但是代码中还是存在比较多优化的地方,例如获取元素或者window的偏移量等可以采取性能更加的方式,以后有时间再优化啦~
<template>
<div class="dialog-wrap">
<div
class="dialog"
ref="dialogRef"
>
<div class="dialog-header">这是一个dialog</div>
<div class="dialog-body">dialog内容区域</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import _ from 'lodash'
const dialogRef = ref<HTMLElement | null>(null)
onMounted(() => {
const dialogEle = dialogRef.value
if (!dialogEle) return
dialogEle.onmousedown = (e: MouseEvent) => {
dialogEle.style.cursor = 'move'
const { offsetWidth, offsetHeight } = dialogEle
const maxX = window.innerWidth - offsetWidth
const maxY = window.innerHeight - offsetHeight
// 拖拽时阻止浏览器默认事件
e.preventDefault()
// 获取初始位置
const transform = window.getComputedStyle(dialogEle).transform
const transformMatrix = transform.slice(7, transform.length - 1).split(', ')
const translateX = transformMatrix[4] ? parseFloat(transformMatrix[4]) : 0
const translateY = transformMatrix[5] ? parseFloat(transformMatrix[5]) : 0
// 获取鼠标位置和拖拽位置的偏差
const deltaX = e.clientX - translateX
const deltaY = e.clientY - translateY
document.onmousemove = _.throttle((e: any) => {
const { clientX, clientY } = e
let left = clientX - deltaX
let top = clientY - deltaY
// 不允许拖拽元素超出页面
left = left > 0 ? (left > maxX ? maxX : left) : 0
top = top > 0 ? (top > maxY ? maxY : top) : 0
// 不允许鼠标超出页面
left = clientX > 0 ? (clientX < window.innerWidth ? left : maxX) : 0
top = clientY > 0 ? (clientY < window.innerHeight ? top : maxY) : 0
dialogEle.style.transform = `translate(${left}px, ${top}px)`
// dialogEle.style.left = `${left}px`
// dialogEle.style.top = `${top}px`
}, 200)
}
document.onmouseup = () => {
console.log('up')
dialogEle.style.cursor = ''
document.onmousemove = null
}
})
</script>
<style lang="scss" scoped>
.dialog-wrap {
width: 100%;
height: 100%;
position: relative;
/*display: flex;
justify-content: center;
align-items: center;*/
.dialog {
position: absolute;
width: 500px;
height: 350px;
font-size: 18px;
color: #fff;
transform: translate3d(0, 0, 1px);
will-change: transform;
-webkit-will-change: transform;
.dialog-header {
width: 100%;
height: 100px;
background: rgb(75, 75, 75);
}
.dialog-body {
width: 100%;
height: 250px;
background: rgb(231, 130, 130);
}
}
}
</style>