前言
    SortableJS 是基于 H5 拖拽 API 实现的一个轻量级 JS 拖拽排序库,它适用于以下一些场景:
容器项目拖动排序:容器列表内的子项目,通过拖动进行位置调换,且具有动画效果;
容器间的项目移动:将一个容器列表中的子项目,拖动到另一个容器列表中(移动/克隆)。
    不论是容器内元素顺序排序,或是两个容器内的元素进行移动,本质上是在通过操作 DOM 来实现。
下面我们先熟悉一下 SortableJS 基本使用。
示例
1、HTML 结构:
<div class="row"> <div id="leftContainer" class="list-group col-6"> <div class="list-group-item">Item 1</div> <div class="list-group-item">Item 2</div> <div class="list-group-item">Item 3</div> <div class="list-group-item">Item 4</div> <div class="list-group-item">Item 5</div> <div class="list-group-item">Item 6</div> </div> <div id="rightContainer" class="list-group col-6"> <div class="list-group-item tinted">Item 1</div> <div class="list-group-item tinted">Item 2</div> <div class="list-group-item tinted">Item 3</div> <div class="list-group-item tinted">Item 4</div> <div class="list-group-item tinted">Item 5</div> <div class="list-group-item tinted">Item 6</div> </div></div>
2、为容器实例化:
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});现在,就可以在容器内进行排序拖动,或者拖动左侧容器元素,添加到右侧容器中。
思路分析
    在看源码之前,还是需要对 H5 拖拽 用法有一定了解,如果不熟悉,直接去看源码很容易就放弃。
    若你对 H5 拖拽 API 比较熟悉,就可以根据 SortableJS 的视图呈现效果,想出个大概思路。
拖拽,首先要搞清楚两个词汇对象:
            拖动元素:作为拖拽元素被拖起(下文叫 dragEl);        
            目标元素:作为拖拽元素即将被放置时的参照物(下文叫 target);        
在 SortableJS 中,拖拽离不开以下几个事件:
            dragstart:作为拖拽元素,按下鼠标开始拖动元素时触发(拖拽周期只触发一次);        
            dragend:作为拖拽元素,当鼠标松开拖放元素时触发(拖拽周期只触发一次);        
            dragover:作为拖拽元素,当拖动元素进行移动,会持续触发,需要在这里取消默认事件,否则元素无法被拖动(松开时元素的预览幽灵图又回去了);        
            drop:作为目标元素,当鼠标松开拖放元素时触发(拖拽周期只触发一次);        
下面我们一起去分析 SortableJS 具体实现。
源码
实例构造函数
从上面的 示例 使用上得知,SortableJS 是一个构造函数,接收容器元素和配置项:
const expando = 'Sortable' + (new Date).getTime();function Sortable(el, options) {  this.el = el; // root element  this.options = options = Object.assign({}, options);  el[expando] = this;  const defaults = {    group: null,    sort: true, // 默认容器可以排序    animation: 0,    removeCloneOnHide: true, // 将一个容器元素拖动至另一个容器后,默认    setData: function (dataTransfer, dragEl) {      dataTransfer.setData('Text', dragEl.textContent);    }  };  // 参数合并  for (var name in defaults) {    !(name in options) && (options[name] = defaults[name]);  }  // 规范 group  _prepareGroup(options);  // 绑定原型方法为私有方法  for (var fn in this) {    if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {      this[fn] = this[fn].bind(this);    }  }  // 绑定指针触摸事件,类似 mousedown  on(el, 'pointerdown', this._prepareDragStart);  on(el, 'dragover', this);  on(el, 'dragenter', this);}初始化示例做了以下几件事件:
            将传入的参数与提供的 默认参数 进行合并;        
            规范传入的 group 格式;        
将原型上的方法绑定在实例对象上,便于使用;
            绑定 pointerdown、dragover、dragenter 事件,其中 pointerdown 可以看作是 dragstart 事件,做了一些拖拽前的准备工作。        
    group 用于两个容器元素的相互拖拽场景,规范 group 核心代码如下:
function _prepareGroup(options) {  function toFn(value, pull) {    return function(to, from) {      let sameGroup = to.options.group.name &&              from.options.group.name &&              to.options.group.name === from.options.group.name;      if (value == null && (pull || sameGroup)) {        return true;      } else if (value == null || value === false) {        return false;      } else if (pull && value === 'clone') {        return value;      } else {        return value === true;      }    };  }  let group = {};  let originalGroup = options.group;  if (!originalGroup || typeof originalGroup != 'object') {    originalGroup = { name: originalGroup };  }  group.name = originalGroup.name;  group.checkPull = toFn(originalGroup.pull, true);  group.checkPut = toFn(originalGroup.put);  options.group = group;}_prepareDragStart 拖动前的准备工作
    当鼠标按下触发 pointerdown 事件时,会保存拖动元素的信息,提供后续使用,并且注册 dragstart 事件:
let oldIndex,  newIndex;let dragEl = null; // 拖拽元素let rootEl = null; // 容器元素let parentEl = null; // 拖拽元素的父节点let nextEl = null; // 拖拽元素下一个元素let activeGroup = null; // options.groupSortable.prototype = {  _prepareDragStart(evt) {    let target = evt.target,      el = this.el,      options = this.options;    oldIndex = index(target);    rootEl = el;    dragEl = target;    parentEl = dragEl.parentNode;    nextEl = dragEl.nextSibling;    activeGroup = options.group;    dragEl.draggable = true; // 设置元素拖拽属性    on(dragEl, 'dragend', this);    on(rootEl, 'dragstart', this._onDragStart);    on(document, 'mouseup', this._onDrop);  },}    on 就是 addEventListener,index 方法用于获取元素在父容器内的索引:
function on(el, event, fn) {  el.addEventListener(event, fn);}function off(el, event, fn) {  el.removeEventListener(event, fn);}function index(el) {  if (!el || !el.parentNode) return -1;  let index = 0;  // 返回元素节点之前的兄弟元素节点(不包括文本节点、注释节点)  while (el = el.previousElementSibling) {    if (el !== Sortable.clone) index++;  }  return index;}    _onDragStart 用于处理 dragstart 事件逻辑,_onDrop 用于处理拖拽结束逻辑,比如这里执行了 dragEl.draggable = true;,那么在 mouseup 鼠标松开后需将 draggable = false。
    这里有趣的一点是 dragend 事件,它的处理函数绑定的是 this 即 Sortable 实例本身,我们都知道实例对象是一个对象,怎么能作为函数使用呢?
    其实 addEventListener 第二参数可以是函数,也可以是对象,当为对象时,需要提有一个 handleEvent 方法来处理事件:
Sortable.prototype = {  handleEvent: function (evt) {    switch (evt.type) {      case 'dragend':        this._onDrop(evt);        break;      case 'dragover':        evt.stopPropagation();        evt.preventDefault();        break;      case 'dragenter':        if (dragEl) {          this._onDragOver(evt);        }        break;    }  },}到这里,整个拖拽流程功能函数都暴露在了眼前:
            _onDragStart 处理 dragstart 拖拽开始工作;        
            _onDragOver 处理拖拽移动到别的元素时工作;        
            _onDrop 处理鼠标拖动结束的收尾工作。        
dragstart
这里做了两件事情:
            clone 一个 dragEl 元素副本,用于两个容器项目移动时使用;        
            触发外部传入的 clone 和 dragstart 事件;        
let cloneEl = null, cloneHidden = null; // clone 元素_onDragStart(evt) {  let dataTransfer = evt.dataTransfer;  let options = this.options;  cloneEl = clone(dragEl);  cloneEl.removeAttribute("id");  cloneEl.draggable = false;  // 设置拖拽数据  if (dataTransfer) {    dataTransfer.effectAllowed = 'move';    options.setData && options.setData.call(this, dataTransfer, dragEl);  }  Sortable.active = this;  Sortable.clone = cloneEl;  _dispatchEvent({    sortable: this,    name: 'clone'  });  _dispatchEvent({    sortable: this,    name: 'start',    originalEvent: evt  });},function clone(el) {  return el.cloneNode(true);}    _dispatchEvent 会通过 new window.CustomEvent 构造一个事件对象,将拖拽元素的信息添加到自定义事件对象上,传递给外部的注册事件函数,大体代码如下:
function dispatchEvent(...params) {  // sortable 没有传,就根据 rootEl 获取 sortable。  sortable = (sortable || (rootEl && rootEl[expando]));  if (!sortable) return;  let evt,    options = sortable.options,    onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);  // 自定义事件,拿到事件对象,满足外部用户传入的事件正常使用  if (window.CustomEvent) {    evt = new CustomEvent(name, {      bubbles: true,      cancelable: true    });  } else {    evt = document.createEvent('Event');    evt.initEvent(name, true, true);  }  evt.to = toEl || rootEl;  evt.from = fromEl || rootEl;  evt.item = targetEl || rootEl;  evt.clone = cloneEl;  evt.oldIndex = oldIndex;  evt.newIndex = newIndex;  // 执行外部传入的事件  if (options[onName]) {    options[onName].call(sortable, evt);  }}    可见,拖拽的核心逻辑不在 dragstart 中,下面我们去看 dragenter 的处理函数 _onDragOver。
dragenter
    SortableJS 的核心逻辑在 _onDragOver 中,拿容器内项目排序为例:当拖动 dragEl 元素,移动到另一个元素上时,会发生两者的位置交换,可见,Sort 的逻辑在这里。
    首先,在实例化对象时绑定了 dragover 和 dragenter 事件,并且通过 handleEvent 将事件逻辑交由 _onDragOver 来处理:
on(el, 'dragover', this);on(el, 'dragenter', this);handleEvent: function (evt) {  switch (evt.type) {    case 'dragover':      evt.stopPropagation();      evt.preventDefault();      break;    case 'dragenter':      if (dragEl) {        this._onDragOver(evt);      }      break;  }},    在 _onDragOver 中,需要注意一点是:假如有两个容器,那就有两个 new Sortable 实例对象,isOwner 将为 false,这是就需要判断拖动容器的 activeGroup.pull(是否允许被移动)和 group.put(是否允许添加拖动过来的元素)。
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});0上面的核心在于下面这一行代码:
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});1                    如果拖拽元素的位置小于目标元素的位置,说明是从上往下拖动,那么将 dragEl 移动到 target.nextSibling 之前;        
            如果拖拽元素的位置大于目标元素的位置,说明是从下往上拖动,那么只需将 dragEl 移动到 target 之前即可;        
            整个移动过程均采用 DOM 操作 insertBefore 来实现。        
    另外如果是两个容器的场景(isOwner = false ),并且拖动元素的容器 activeGroup.pull = clone,需要将 dragstart 创建的 clone 元素渲染到容器中:
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});2drop
    drop 主要做一些收尾工作,如将 dragEl.draggable = false,移除绑定的 mouseup、dragstart、dragend 事件,触发用户传入的 sort、end 事件等。
    不过注意,虽然起名叫 drop,触发的事件确是 dragend。
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});3动画
如果想在拖动排序中有一定的 animation 动画效果,可以配置动画属性,属性值是动画持续时长:
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});4    动画的时机也是在 dragenter 中,大致的思路如下:
1、记录:记录容器子项位置信息
            在操作 DOM 移动 dragEl 之前,记录容器内所有子项的位置;        
            进行 DOM 操作进行位置交换,DOM 操作本身没有动画;        
这时再去记录一次移动后的容器内所有子项的位置;
2、执行:有了上面几步的操作,接下来就可以根据移动前后的位置进行动画操作
            通过 translate 先让元素立刻回到移动前的位置;        
            此时给元素自身设置过度效果 transform;        
            这时候就可以通过 translate 让元素回到移动之后的位置。        
大致实现如下:
new Sortable(leftContainer, {  group: {    name: 'group',    pull: 'clone',    put: true  },});new Sortable(rightContainer, {  group: 'group',});5最后
    本文以探索 SortableJS 拖拽思路为主线,去了解业界开源拖拽库的设计与思路。感谢阅读。