css3位移属性实现元素拖动

324 阅读3分钟

一)前言

元素拖动可以用常规的left\top定位属性实现,但是也可以使用css3的位移属性实现,且用transform实现相对于原本的left/top定位方式更加的节省性能,因为left/top会不断的引起重绘、回流与重组,而transform是在单独的图层中进行平移,最后再合并图层,所以不会引起页面单独的回流,且translate3d更有gpu加速,所以性能更优。

(二) 原因探究

可以利用谷歌的调试工具具体看下二者实现方式的不同:

1、比较

transform: translate

transform-drag.gif

image-20230304100955403.png

position:left/top

position-drag.gif

image-20230304101235764.png

由上图的区别可以明显对比出,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擅长的领域:

  1. 绘制东西到屏幕上
  2. 一次次绘制同一张位图到屏幕上
  3. 绘制同一张位图到不同的位置、旋转角度和缩放比例

当然还有栅格化等这里就不细究了....

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>