拖拽排序-性能一般般版本

417 阅读3分钟

拖拽排序

背景

最近自己鼓捣一个思维导图,涉及一个拖动排序的过程。在掘金看到了神光大佬的写的文章[1],有很大的启发。在原文的基础上,根据自己的思路进行修改,并写了个demo。

效果

drag1.gif

drag2.gif

思路

  • 直接修改dom无法触发transiton,因此通过translate对元素位置进行修改,达到一个平滑的transition,最后再进行一个dom操作。
  • 增加一个views数组,用于表示页面上元素的被拖动到哪里,在拖拽过程中不断修改这个views,使得和视觉顺序一致,然后通过views进一步确定哪些元素需要translate。(虽然增加了一个views数组会增加空间复杂度,但是实现起来就简单很多了)
  • 拖拽结束后,修改dom,使视觉顺序和dom保持顺序一致。

views数组定义和更新过程

  • views[i]记录的是原始的位置,下标是最终的位置。 比如把0号元素拖动到5号位置,view数组的变化
i    befoer views[i]    after views[i]
0        0                  1  
1        1                  2
2        2                  3
3        3                  4
4        4                  5
5        5                  0

  initViews() {
    this.views = Array(this.containerElement.children.length)
      .fill(0)
      .map((_, index) => index);
  }


  updateViews(from, to) {
    const [removed] = this.views.splice(from, 1);
    this.views.splice(to, 0, removed);
    this.translate();
  }

根据views, 很容易计算translate

  translate() {
    const children = this.containerElement.children;
    this.views.forEach((last, now) => {
      const child = children[last];
      const fromRect = this.rects[last];
      const toRect = this.rects[now];
      if (last !== now) {
        child.style.transform = `translate3d(${toRect.x - fromRect.x}px, ${
          toRect.y - fromRect.y
        }px,0)`;
      } else {
        child.style.transform = `translate3d(0,0,0)`;
      }
    });
  }

实现

graph TD
onDragStart -->初始化--> onDdrag --> 更新views数组并进行translate & 更新cloneElement位置
-->onDragEnd --> 清理

完整代码

class Draggable {
  // 容器
  containerElement = null;
  // 几何信息
  rects = null;
  // 视图
  views = null;
  // 拖动元素
  dragElement = null;
  // 是否按下鼠标
  isDragging = false;
  // 上一次释放的位置
  lastDropIndex = -1;

  // 记录鼠标移动位置
  lastPoint = {
    x: 0,
    y: 0,
  };
  // cloneElement 位置
  position = {
    left: 0,
    top: 0,
  };
  // cloneElement
  cloneElement = null;

  constructor(selector) {
    this.containerElement = document.querySelector(selector);
    this.initRects();
    this.initViews();
    this.on();
  }

  initRects() {
    this.rects = [...this.containerElement.children].map((child) => {
      return child.getBoundingClientRect();
    });
  }
  initViews() {
    this.views = Array(this.containerElement.children.length)
      .fill(0)
      .map((_, index) => index);
  }

  updateViews(from, to) {
    const [removed] = this.views.splice(from, 1);
    this.views.splice(to, 0, removed);
    this.translate();
  }
  translate() {
    const children = this.containerElement.children;
    this.views.forEach((last, now) => {
      const child = children[last];
      const fromRect = this.rects[last];
      const toRect = this.rects[now];
      if (last !== now) {
        child.style.transform = `translate3d(${toRect.x - fromRect.x}px, ${
          toRect.y - fromRect.y
        }px,0)`;
      } else {
        child.style.transform = `translate3d(0,0,0)`;
      }
    });
  }

  commitDOM(from, to) {
    const children = this.containerElement.children;
    if (from === to) return;
    let anchor = null;
    if (from < to) {
      anchor = children[to].nextSibling;
    } else {
      anchor = children[to];
    }
    this.containerElement.insertBefore(this.dragElement, anchor);
  }

  getPointerIndex(e) {
    /* 根据鼠标位置计算出当前item位置 */
    const { x, y } = e;
    for (const [i, rect] of this.rects.entries()) {
      if (
        rect.left <= x &&
        x <= rect.right &&
        rect.top <= y &&
        y <= rect.bottom
      ) {
        return i;
      }
    }
    /* 不在items里面,如items间隔 */
    return -1;
  }

  onDragStart(e) {
    const target = e.target;
    // 判断是不是draggable
    if (target.dataset.drag !== "true") {
      return;
    }
    this.isDragging = true;

    this.dragElement = e.target;

    this.lastDropIndex = [...this.containerElement.children].indexOf(
      this.dragElement
    );
    this.lastPoint.x = e.x;
    this.lastPoint.y = e.y;

    for (const item of this.containerElement.children) {
      item.style.transition = "transform 500ms";
    }
    // cloneElement
    const rect = this.rects[this.lastDropIndex];
    this.position.left = rect.x;
    this.position.top = rect.y;
    this.cloneElement = this.dragElement.cloneNode(true);
    this.cloneElement.style.transition = "none";
    this.cloneElement.style.position = "fixed";
    this.cloneElement.style.top = 0;
    this.cloneElement.style.left = 0;
    this.cloneElement.style.width = `${rect.width}px`;
    this.cloneElement.style.height = `${rect.height}px`;
    this.cloneElement.style.transform = `translate3d(${this.position.left}px, ${this.position.top}px,0)`;
    this.cloneElement.style.pointerEvents = "none";
    this.cloneElement.style.backgroundColor = "pink";
    document.body.appendChild(this.cloneElement);
    //
    this.dragElement.classList.add("moving");
  }
  onDrag(e) {
    if (!this.isDragging) {
      return;
    }

    // cloneElement
    this.position.left += e.x - this.lastPoint.x;
    this.position.top += e.y - this.lastPoint.y;
    this.cloneElement.style.transform = `translate3d(${this.position.left}px, ${this.position.top}px,0)`;
    this.lastPoint.x = e.x;
    this.lastPoint.y = e.y;

    // translate
    const dragIndex = this.lastDropIndex;
    const dropIndex = this.getPointerIndex(e);
    if (dropIndex < 0) return;
    this.lastDropIndex = dropIndex;
    this.updateViews(dragIndex, dropIndex);
  }
  onDragEnd(e) {
    if (!this.isDragging) {
      return;
    }
    this.initViews();
    const from = [...this.containerElement.children].indexOf(this.dragElement);
    this.commitDOM(from, this.lastDropIndex);
    for (const item of this.containerElement.children) {
      // commitDOM之后,dom和视图是一致的,因此不需要translate&transition
      item.style.transition = "none"; // 否则设置translate会有过度效果
      item.style.transform = "translate3d(0px, 0px, 0px)";
    }
    this.isDragging = false;
    this.dragElement.classList.remove("moving");
    this.cloneElement.remove();
  }

  on() {
    this.containerElement.addEventListener(
      "pointerdown",
      this.onDragStart.bind(this)
    );
    this.containerElement.addEventListener(
      "pointermove",
      this.onDrag.bind(this)
    );
    this.containerElement.addEventListener(
      "pointerup",
      this.onDragEnd.bind(this)
    );
  }
}

const d = new Draggable(".container");

总结

  • 学习的大佬的内容并根据自己的思路进行修改
  • 使用事件委托方式,不确定是否能提高性能,因为拖拽过程中是会一直触发pointemove, 改成在item里面绑定enterleave事件,或许能性能更好?

参考

[1] 手写一个性能较好的拖拽排序