起因

众所周知,sortablejs是一个根据dom操作实现拖拽功能的插件,十分好用。但是当我们使用vue框架来进行开发的时候,就和vue所强调的数据驱动视图不符合了,所以这就有了vue-draggable这个插件的存在

原理

vue-draggable插件本质上是实现了一个全局组件vue-draggable,它的created方法只是对配置做了检测,所以我们从它的mounted声明周期开始看起

// 这些方法 vue-draggable 组件内部也要实现对应的方法
const eventsListened = ["Start", "Add", "Remove", "Update", "End"];
// 这些不需要,直接发送出去就行
const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"];function emit(evtName, evtData) {this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
}function delegateAndEmit(evtName) {return evtData => {// 如果说拖动组件内是有数据的// 就调用 vue-draggable 内相应的方法if (this.realList !== null) {this["onDrag" + evtName](evtData);}// 触发用户设置的回调函数emit.call(this, evtName, evtData);};
}mounted() {this.noneFunctionalComponentMode =this.getTag().toLowerCase() !== this.$el.nodeName.toLowerCase() &&!this.getIsFunctional();if (this.noneFunctionalComponentMode && this.transitionMode) {throw new Error(`Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}`);}const optionsAdded = {};// 这里eventsListened.forEach(elt => {optionsAdded["on" + elt] = delegateAndEmit.call(this, elt);});// 将sortablejs的回调emit出去eventsToEmit.forEach(elt => {optionsAdded["on" + elt] = emit.bind(this, elt);});// 在 vue-draggable 标签上定义的属性// 也就是 sortablejs 的配置项// 因为 vue-draggable 是一个组件const attributes = Object.keys(this.$attrs).reduce((res, key) => {res[camelize(key)] = this.$attrs[key];return res;}, {});// 合并选项const options = Object.assign({}, this.options, attributes, optionsAdded, {onMove: (evt, originalEvent) => {return this.onDragMove(evt, originalEvent);}});// 没配置默认所有都可以拖拽!("draggable" in options) && (options.draggable = ">*");this._sortable = new Sortable(this.rootContainer, options);// 最关键this.computeIndexes();},

mounted生命周期函数主要做了以下事
1.配置sortablejs相关的回调函数,因为eventsListened里面的方法回调vue-draggable组件也需要使用。所以定义了个delegateAndEmit方法
2.合并配置选项,实例化sortablejs
3.(最关键)计算列表项 vnode 在真实 dom 对象中的位置,并保存一个 index 数组做映射

computeIndexes

computeIndexes相关方法如下

computeIndexes() {this.$nextTick(() => {this.visibleIndexes = computeIndexes(this.getChildrenNodes(),this.rootContainer.children,this.transitionMode,this.footerOffset);});
},// 计算列表项 vnode 在真实 dom 对象中的位置,并返回一个 index 数组
// slots -> slots.default 列表数据对应的 vnode 数组
// children -> vue-draggable 组件 dom 元素的 children ,有可能会包括其它元素,比如 header slot 或者 footer slot
// isTransition 就是我们可以在列表外面包裹一层 transition-group 组件,只是获取和判断逻辑稍微变下,有兴趣可以了解下
function computeIndexes(slots, children, isTransition, footerOffset) {if (!slots) {return [];}const elmFromNodes = slots.map(elt => elt.elm);const footerIndex = children.length - footerOffset;const rawIndexes = [...children].map((elt, idx) =>idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt));return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes;
}

它主要作用就是遍历 dom 的 children,然后找到它在虚拟DOM里面的位置,这样我们就能找到这条属性相应的位置了

获取了visibleIndexes后我们就可以看vue-draggable内拖拽触发相应的回调方法

// 拖拽数据到新的列表中时触发
// add 是相对于新列表,remove是相对于旧列表
onDragRemove(evt) {// 将 evt.item 放到旧列表中insertNodeAt(this.rootContainer, evt.item, evt.oldIndex);// clone 模式会复制一个 clone 节点替换旧列表的 item 使旧列表看起来不会变化// 会有 evt.clone 和 evt.item 字段// 上面已经将 item 字段的节点也塞到旧列表中了// 所以清除掉 clone 节点就行if (evt.pullMode === "clone") {removeNode(evt.clone);return;}// 从旧的列表中清除数据const oldIndex = this.context.index;this.spliceList(oldIndex, 1);const removed = { element: this.context.element, oldIndex };this.resetTransitionData(oldIndex);this.emitChanges({ removed });
},// 同个列表中列表项更换位置触发
onDragUpdate(evt) {// 会移除当前节点removeNode(evt.item);insertNodeAt(evt.from, evt.item, evt.oldIndex);const oldIndex = this.context.index;const newIndex = this.getVmIndex(evt.newIndex);// 更改数据触发 vue 的视图更新this.updatePosition(oldIndex, newIndex);const moved = { element: this.context.element, oldIndex, newIndex };this.emitChanges({ moved });
},

里面最重要的就是getVmIndex方法,它的代码如下

// 根据 sortablejs 返回的 domIndex 的位置来找到数据在列表 vnode 中的位置
// 这样就可以找到数据在列表数据里的位置
getVmIndex(domIndex) {const indexes = this.visibleIndexes;const numberIndexes = indexes.length;return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex];
},

就是通过我们前面的visibleIndexes数组,然后通过sortablejs返回的domIndex来找到真实数据中对应的位置

最后调用updatePosition方法,代码如下

// 通过数据更新通知 vue 更新位置
updatePosition(oldIndex, newIndex) {const updatePosition = list =>list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);this.alterList(updatePosition);
},

通过更改我们传进的数组位置来触发vue的视图更新,从而也就达成数据和视图的双向绑定~

下面是完整代码,有兴趣可以自己看一下~

import Sortable from "sortablejs";
// insertNodeAt 插入节点
// removeNode 移除节点
import { insertNodeAt, camelize, console, removeNode } from "./util/helper";function buildAttribute(object, propName, value) {if (value === undefined) {return object;}object = object || {};object[propName] = value;return object;
}function computeVmIndex(vnodes, element) {return vnodes.map(elt => elt.elm).indexOf(element);
}// 计算列表项 vnode 在真实 dom 对象中的位置,并返回一个 index 数组
// slots -> slots.default 列表数据对应的 vnode数组
// children -> vue-draggable组件 dom 元素的 children ,有可能会包括其它元素,比如 header slot 或者 footer slot
function computeIndexes(slots, children, isTransition, footerOffset) {if (!slots) {return [];}const elmFromNodes = slots.map(elt => elt.elm);const footerIndex = children.length - footerOffset;const rawIndexes = [...children].map((elt, idx) =>idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt));return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes;
}function emit(evtName, evtData) {this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
}function delegateAndEmit(evtName) {return evtData => {// 如果说拖动组件内是有数据的// 就调用 vue-draggable 内相应的方法if (this.realList !== null) {this["onDrag" + evtName](evtData);}// 触发用户设置的回调函数emit.call(this, evtName, evtData);};
}function isTransitionName(name) {return ["transition-group", "TransitionGroup"].includes(name);
}// 判断第一个 slots.default 第一个 子vnode 是不是transition-group
function isTransition(slots) {if (!slots || slots.length !== 1) {return false;}const [{ componentOptions }] = slots;if (!componentOptions) {return false;}return isTransitionName(componentOptions.tag);
}function getSlot(slot, scopedSlot, key) {return slot[key] || (scopedSlot[key] ? scopedSlot[key]() : undefined);
}function computeChildrenAndOffsets(children, slot, scopedSlot) {let headerOffset = 0;let footerOffset = 0;const header = getSlot(slot, scopedSlot, "header");if (header) {headerOffset = header.length;children = children ? [...header, ...children] : [...header];}const footer = getSlot(slot, scopedSlot, "footer");if (footer) {footerOffset = footer.length;children = children ? [...children, ...footer] : [...footer];}return { children, headerOffset, footerOffset };
}function getComponentAttributes($attrs, componentData) {let attributes = null;const update = (name, value) => {attributes = buildAttribute(attributes, name, value);};const attrs = Object.keys($attrs).filter(key => key === "id" || key.startsWith("data-")).reduce((res, key) => {res[key] = $attrs[key];return res;}, {});update("attrs", attrs);if (!componentData) {return attributes;}const { on, props, attrs: componentDataAttrs } = componentData;update("on", on);update("props", props);Object.assign(attributes.attrs, componentDataAttrs);return attributes;
}// 这些方法 vue-draggable 组件内部也要实现对应的方法
const eventsListened = ["Start", "Add", "Remove", "Update", "End"];
// 这些不需要,直接发送出去就行
const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"];
const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map(evt => "on" + evt
);
var draggingElement = null;const props = {options: Object,list: {type: Array,required: false,default: null},value: {type: Array,required: false,default: null},noTransitionOnDrag: {type: Boolean,default: false},clone: {type: Function,default: original => {return original;}},element: {type: String,default: "div"},tag: {type: String,default: null},move: {type: Function,default: null},componentData: {type: Object,required: false,default: null}
};const draggableComponent = {name: "draggable",inheritAttrs: false,props,data() {return {transitionMode: false,noneFunctionalComponentMode: false};},render(h) {const slots = this.$slots.default;// isTransitionthis.transitionMode = isTransition(slots);const { children, headerOffset, footerOffset } = computeChildrenAndOffsets(slots,this.$slots,this.$scopedSlots);this.headerOffset = headerOffset;this.footerOffset = footerOffset;console.log(headerOffset, footerOffset);const attributes = getComponentAttributes(this.$attrs, this.componentData);return h(this.getTag(), attributes, children);},created() {if (this.list !== null && this.value !== null) {console.error("Value and list props are mutually exclusive! Please set one or another.");}if (this.element !== "div") {console.warn("Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props");}if (this.options !== undefined) {console.warn("Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props");}},mounted() {this.noneFunctionalComponentMode =this.getTag().toLowerCase() !== this.$el.nodeName.toLowerCase() &&!this.getIsFunctional();if (this.noneFunctionalComponentMode && this.transitionMode) {throw new Error(`Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}`);}const optionsAdded = {};eventsListened.forEach(elt => {optionsAdded["on" + elt] = delegateAndEmit.call(this, elt);});eventsToEmit.forEach(elt => {optionsAdded["on" + elt] = emit.bind(this, elt);});// 在 vue-draggable 标签上定义的属性// 也就是 sortablejs 的配置项// 因为 vue-draggable 是一个组件const attributes = Object.keys(this.$attrs).reduce((res, key) => {res[camelize(key)] = this.$attrs[key];return res;}, {});// 合并选项const options = Object.assign({}, this.options, attributes, optionsAdded, {onMove: (evt, originalEvent) => {return this.onDragMove(evt, originalEvent);}});// 没配置默认所有都可以拖拽!("draggable" in options) && (options.draggable = ">*");this._sortable = new Sortable(this.rootContainer, options);this.computeIndexes();},beforeDestroy() {if (this._sortable !== undefined) this._sortable.destroy();},computed: {// 获取当前容器rootContainer() {return this.transitionMode ? this.$el.children[0] : this.$el;},// 传入的 list 或 valuerealList() {return this.list ? this.list : this.value;}},watch: {options: {handler(newOptionValue) {this.updateOptions(newOptionValue);},deep: true},$attrs: {handler(newOptionValue) {this.updateOptions(newOptionValue);},deep: true},realList() {this.computeIndexes();}},methods: {getIsFunctional() {const { fnOptions } = this._vnode;return fnOptions && fnOptions.functional;},getTag() {return this.tag || this.element;},// 用户改变数据动态更新updateOptions(newOptionValue) {for (var property in newOptionValue) {const value = camelize(property);if (readonlyProperties.indexOf(value) === -1) {this._sortable.option(value, newOptionValue[property]);}}},// 获取用户列表数据生成的 vnodegetChildrenNodes() {if (this.noneFunctionalComponentMode) {return this.$children[0].$slots.default;}const rawNodes = this.$slots.default;return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes;},computeIndexes() {this.$nextTick(() => {this.visibleIndexes = computeIndexes(this.getChildrenNodes(),this.rootContainer.children,this.transitionMode,this.footerOffset);});},// 根据 dom 节点获取相关的数据和位置getUnderlyingVm(htmlElt) {const index = computeVmIndex(this.getChildrenNodes() || [], htmlElt);if (index === -1) {//Edge case during move callback: related element might be//an element different from collectionreturn null;}const element = this.realList[index];console.log(element)return { index, element };},getUnderlyingPotencialDraggableComponent({ __vue__: vue }) {if (!vue ||!vue.$options ||!isTransitionName(vue.$options._componentTag)) {if (!("realList" in vue) &&vue.$children.length === 1 &&"realList" in vue.$children[0])return vue.$children[0];return vue;}return vue.$parent;},emitChanges(evt) {this.$nextTick(() => {this.$emit("change", evt);});},alterList(onList) {if (this.list) {onList(this.list);return;}const newList = [...this.value];onList(newList);this.$emit("input", newList);},spliceList() {const spliceList = list => list.splice(...arguments);this.alterList(spliceList);},// 通过数据更新通知 vue 更新位置updatePosition(oldIndex, newIndex) {const updatePosition = list =>list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);this.alterList(updatePosition);},getRelatedContextFromMoveEvent({ to, related }) {const component = this.getUnderlyingPotencialDraggableComponent(to);if (!component) {return { component };}const list = component.realList;const context = { list, component };if (to !== related && list && component.getUnderlyingVm) {const destination = component.getUnderlyingVm(related);if (destination) {return Object.assign(destination, context);}}return context;},// 根据 sortablejs 返回的 domIndex 的位置来找到数据在列表 vnode 中的位置// 这样就可以找到数据在列表数据里的位置getVmIndex(domIndex) {const indexes = this.visibleIndexes;const numberIndexes = indexes.length;return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex];},getComponent() {return this.$slots.default[0].componentInstance;},resetTransitionData(index) {if (!this.noTransitionOnDrag || !this.transitionMode) {return;}var nodes = this.getChildrenNodes();nodes[index].data = null;const transitionContainer = this.getComponent();transitionContainer.children = [];transitionContainer.kept = undefined;},// 拖拽开始,记录一下当前的数据和位置onDragStart(evt) {this.context = this.getUnderlyingVm(evt.item);evt.item._underlying_vm_ = this.clone(this.context.element);draggingElement = evt.item;},// 拖拽数据到新的列表中时触发// 在新的列表中塞进数据,实现数据与视图同步onDragAdd(evt) {const element = evt.item._underlying_vm_;if (element === undefined) {return;}removeNode(evt.item);const newIndex = this.getVmIndex(evt.newIndex);this.spliceList(newIndex, 0, element);this.computeIndexes();const added = { element, newIndex };this.emitChanges({ added });},// 拖拽数据到新的列表中时触发// add 是相对于新列表,remove是相对于旧列表onDragRemove(evt) {// 将 evt.item 放到旧列表中insertNodeAt(this.rootContainer, evt.item, evt.oldIndex);// clone 模式会复制一个 clone 节点替换旧列表的 item 使旧列表看起来不会变化// 会有 evt.clone 和 evt.item 字段// 上面已经将 item 字段的节点也塞到旧列表中了// 所以清除掉 clone 节点就行if (evt.pullMode === "clone") {removeNode(evt.clone);return;}// 从旧的列表中清除数据const oldIndex = this.context.index;this.spliceList(oldIndex, 1);const removed = { element: this.context.element, oldIndex };this.resetTransitionData(oldIndex);this.emitChanges({ removed });},// 同个列表中列表项更换位置触发onDragUpdate(evt) {// 会移除当前节点removeNode(evt.item);insertNodeAt(evt.from, evt.item, evt.oldIndex);const oldIndex = this.context.index;const newIndex = this.getVmIndex(evt.newIndex);// 更改数据触发 vue 的视图更新this.updatePosition(oldIndex, newIndex);const moved = { element: this.context.element, oldIndex, newIndex };this.emitChanges({ moved });},updateProperty(evt, propertyName) {evt.hasOwnProperty(propertyName) &&(evt[propertyName] += this.headerOffset);},computeFutureIndex(relatedContext, evt) {if (!relatedContext.element) {return 0;}const domChildren = [...evt.to.children].filter(el => el.style["display"] !== "none");const currentDOMIndex = domChildren.indexOf(evt.related);const currentIndex = relatedContext.component.getVmIndex(currentDOMIndex);const draggedInList = domChildren.indexOf(draggingElement) !== -1;return draggedInList || !evt.willInsertAfter? currentIndex: currentIndex + 1;},onDragMove(evt, originalEvent) {const onMove = this.move;if (!onMove || !this.realList) {return true;}const relatedContext = this.getRelatedContextFromMoveEvent(evt);const draggedContext = this.context;const futureIndex = this.computeFutureIndex(relatedContext, evt);Object.assign(draggedContext, { futureIndex });const sendEvt = Object.assign({}, evt, {relatedContext,draggedContext});return onMove(sendEvt, originalEvent);},onDragEnd() {this.computeIndexes();draggingElement = null;}}
};if (typeof window !== "undefined" && "Vue" in window) {window.Vue.component("draggable", draggableComponent);
}export default draggableComponent;

浅谈vue-draggable原理相关推荐

  1. 浅谈:Spring Boot原理分析,切换内置web服务器,SpringBoot监听项目(使用springboot-admin),将springboot的项目打成war包

    浅谈:Spring Boot原理分析(更多细节解释在代码注释中) 通过@EnableAutoConfiguration注解加载Springboot内置的自动初始化类(加载什么类是配置在spring.f ...

  2. 浅谈前端路由原理hash和history

    浅谈前端路由原理hash和history

  3. 浅谈Vue.js的优势

    写在前面 今天小梦跟小伙伴们简简单单聊一下Vue.js的优势.小梦也是刚刚接触Vue.js,在学习一门新的技术之前,我们当然要了解其优势,知道优势在哪更加有利于我们去学习并转换为自己的储备. 浅谈Vu ...

  4. 浅谈“三层结构”原理与用意(转帖)

    浅谈"三层结构"原理与用意 序 在刚刚步入"多层结构"Web应用程序开发的时候,我阅读过几篇关于"asp.net三层结构开发"的文章.但其多 ...

  5. 父子组建传值_浅谈Vue父子组件和非父子组件传值问题

    本文介绍了浅谈Vue父子组件和非父子组件传值问题,分享给大家,具体如下: 1.如何创建组件 1.新建一个组件,如:在goods文件夹下新建goodsList.vue goodsList组件 expor ...

  6. vue 给checkbox 赋值_浅谈vue中关于checkbox数据绑定v-model指令的个人理解

    vue.js为开发者提供了很多便利的指令,其中v-model用于表单的数据绑定很常见, 下面是最常见的例子: {{msg}} js里data初始化数据 new Vue({ el: "#myA ...

  7. java get请求 数组,浅谈vue中get请求解决传输数据是数组格式的问题

    qs的stringify接收2个参数,第一个参数是需要序列化的对象,第二个参数是转化格式,一般默认格式是给出明确的索引,如:arr[0]=1&arr[1]=2 //indices是index的 ...

  8. anchor锚点 antvue_浅谈vue 锚点指令v-anchor的使用

    如下所示: export default { inserted: function(el, binding) { el.onclick = function() { let total; if (bi ...

  9. 浅谈Vue中的虚拟DOM

    Virtual DOM 是JavaScript按照DOM的结构来创建的虚拟树型结构对象,是对DOM的抽象,比DOM更加轻量型 为啥要使用Virtual DOM 当然是前端优化方面,避免频繁操作DOM, ...

  10. svc的参考文献_浅谈SVC的原理及作用

    浅谈 SVC 的原理及作用 超(特)高压运行检修公司 自贡中心 涂洪骏 摘要: 介绍了静止补偿器 (SVC) 的工作特性.基本原理.运行方式,重点针对 SVC 的作用进行了分析. 关键词 :静止补偿器 ...

最新文章

  1. php运行ecshop,ecshop2.x代码执行
  2. sql大于某个时间_学习SQL-复杂查询
  3. echarts 自定义图表
  4. C 中的内存操作函数-memcpy 等(to be continued)
  5. wxWidgets:窗口大小概述
  6. Spring整合mybatis中的sqlSession是如何做到线程隔离的?
  7. java中什么是匿名接口_Java中接口(interface)和匿名类
  8. Pandas系列(十三)分层索引MultiIndex
  9. 2008-05-23
  10. Discuz!NT3.0博客扩展
  11. 阿里云云计算助理工程师认证(ACA)50个资源合集和备考题库
  12. 微软TTS语音引擎实现文本朗读
  13. 使用telnet发送email(内嵌图片,附件)
  14. mysql分区数据覆盖_彻底搞懂MySQL分区
  15. “黑暗潜伏者” -- 手机病毒新型攻击方式
  16. Android drawable.setBounds()+设置RadioButton的图片大小和位置
  17. PHP编写modbus,php – CRC-CCITT转CRC16 Modbus实现
  18. 条件求和:SUMIF、SUMIFS函数
  19. 黑产系列01-如何发现黑产情报
  20. 智源社区AI周刊No.105:谷歌FLAN-T5 30亿参数即超越GPT-3性能;Copilot推出语音编程新功能,但面临诉讼...

热门文章

  1. ReactNative初级到项目实战-李文瀚-专题视频课程
  2. Error: error from slirp4netns while setting up port redirection: map[desc:bad request: add_hostfwd:
  3. 弘辽科技:淘宝词根和关键词有什么联系?如何优化?
  4. python量化实战 顾比倒数线_顾比倒数线 金字塔实现的顾比倒数线(非常精简) 金字塔软件 源码...
  5. ms17-010 php版本,ms17-010补丁xp版
  6. 关于cmd命令提示符的复制和粘贴
  7. 激光三角法测量原理-直射式斜射式
  8. FF900R12ME7B11NPSA1,FF900R12ME7WB11BPSA1 1200V 双IGBT模块
  9. Excel添加数据分析插件
  10. 金山毒霸四月安全趋势 继续关注网页挂马