Vue.Draggable 踩坑:add 事件与 change 事件中 newIndex 字段不同之谜

背景

  最近在弄自定义表单,需要拖动组件进行表单设计,所以用到了 Vue.Draggable(中文文档)。Vue.Draggable 是一款基于 Sortable.js 实现的 vue 拖拽插件,文档挺简单的,用起来也方便,但没想到接下来给我遇到了灵异事件…

坑的表现

  当我写完了由配置对象到组件的渲染逻辑之后,便开始了阶段性测试。我先是拖入了一个输入框,它正常的渲染了出来,并且各项功能都很正常。
在这里插入图片描述

  然后我又拖了个文本域进去,随手把它放在输入框的下面。

在这里插入图片描述

  结果意想不到的事发生了,文本域居然跑到了输入框的上面去了,我惊呆了…

在这里插入图片描述
  印象中拖入放置时元素在列表中的位置是 Vue.Draggable 自己维护的啊,我没做什么控制,怎么可能出问题呢?满脑子疑惑的我又拖了个文本域放在输入框下面,结果它有一次惊呆了我,它正常了,没有跑到输入框上面去…

  我刷新页面打算重新试一下。

    ● 第一步,拖入一个输入框,正常。
    ● 第二步,拖入一个文本域放在输入框下面,不正常,跑上面去了。
    ● 第三步,再次拖入一个文本域放在输入框下面,正常。

  好家伙,看来按这个步骤是百分百重现了。老老实实去检查代码,确认没有手动维护过 Vue.Draggable 中的 list。在 add 事件中打印 event.newIndex (以下称 addEvent.newIndex),发现 addEvent.newIndex 的值是正常的,但是却与新增元素在 list 中的下标不一致,又在 change 事件中打印 newIndex (以下称 changeEvent.newIndex),发现 changeEvent.newIndex 却是指向新元素在 list 中的位置。

  但是 changeEvent.newIndex 的值不对啊!它应该跟 addEvent.newIndex 一样才对啊!文本域应该在输入框的下面才对啊!啊啊啊!!!难道我发现了 Vue.Draggable 的 BUG?

填坑

  结论直达

  两个事件中的 newIndex 完全是由 Vue.Draggable 自身维护的,要想找到导致它俩不同的原因,只能去看看 Vue.Draggable 的源码了。于是我拉取了 Vue.Draggable 的源码,打算来研究一下。值得庆幸的是 Vue.Draggable 的源码很少,只有 400 多行,读起来比较简单。

  我很快找到了下面处理 add 事件的代码。

// Vue.Draggable 源码// onDragAdd 方法是 Draggable 组件内部方法,它调用之后才会 emit Draggable 组件的 add 事件
// 可以在源码中搜索 delegateAndEmit 查找绑定事件的位置
// vue 组件 methods 选项中的方法
onDragAdd(evt) {const element = evt.item._underlying_vm_;if (element === undefined) {return;}removeNode(evt.item);// evt.newIndex 是 add 事件中的 newIndexconst newIndex = this.getVmIndex(evt.newIndex);this.spliceList(newIndex, 0, element);this.computeIndexes();// added.newIndex 是 change 事件中的 newIndexconst added = { element, newIndex };this.emitChanges({ added });
},

  从上面的代码中可以看出,change 事件中的 newIndexadd 事件中的 newIndex 经由 this.getVmIndex() 方法加工而来的。那么我们看一下 this.getVmIndex() 方法做了什么加工导致了它们的不一样。

// Vue.Draggable 源码// added.newIndex 依赖于 this.visibleIndexes (一个数组),当新元素的下标小于 this.visibleIndexes 的长度减一时,返回 this.visibleIndexes 的长度,否则返回 this.visibleIndexes 中下标为 domIndex 的值
/*** vue 组件 methods 选项中的方法,计算并返回 change 事件中的 newIndex* @param {number} domIndex - add 事件中的 newIndex* @returns {number}*/
getVmIndex(domIndex) {const indexes = this.visibleIndexes;const numberIndexes = indexes.length;return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex];
},

  getVmIndex() 方法用于计算 changeEvent.newIndex。它的参数 domIndex 即为 addEvent.newIndex

  从上面的代码可以看出 changeEvent.newIndex 还依赖于 this.visibleIndexes(一个数组),当 domIndex(新元素的下标)大于 this.visibleIndexes 的长度 - 1(最后一个元素的下标)时,changeEvent.newIndexthis.visibleIndexes的长度(最后一个元素的下标 + 1),否则为 this.visibleIndexes 中下标为 domIndex 的值。

  看来还要弄明白 this.visibleIndexes 是什么,下面的代码说明了 this.visibleIndexes 的由来。

// vue 组件 methods 选项中的方法,
computeIndexes() {this.$nextTick(() => {// 这个 computeIndexes 并不是在 methods 中声明的,因此调用时没有使用 thisthis.visibleIndexes = computeIndexes(this.getChildrenNodes(),this.rootContainer.children,this.transitionMode,this.footerOffset);});
},/*** 计算 this.visibleIndexes 列表* @param {Array<VNode>} slots - isTransition 为 true 时,表示 TransitionGroup 的默认插槽,否则表示 draggable 组件的默认插槽* @param {Array<Node>} children - isTransition 为 true 时,表示 TransitionGroup 子元素列表,否则表示 draggable 组件子元素列表* @param {boolean} isTransition - 是否使用了 TransitionGroup 组件* @param {number} footerOffset - footer 插槽根元素的个数,没有使用 footer 插槽时为 0* @returns*/
function computeIndexes(slots, children, isTransition, footerOffset) {if (!slots) {return [];}const elmFromNodes = slots.map(elt => elt.elm);const footerIndex = children.length - footerOffset;// rawIndexes 列表表示显示的节点,其虚拟节点在 slots 中的位置const rawIndexes = [...children].map((elt, idx) => {return idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt);});// 如果使用了 TransitionGroup 组件,则将 children 中有而 slots 中没有的过滤掉return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes;
}

  由上面的代码可以看出 rawIndexes 数组表示:显示的节点,其虚拟节点在 slots 中的位置。rawIndexes 元素的下标表示节点在 children 中的下标,元素的值表示节点在 slots 中的下标(如果节点在 footer 之后,则值为 slots 的长度)。

  现在我们再来看 getVmIndex() 方法,this.visibleIndexes 是由 slotschildren 维护的,而它决定了 changeEvent.newIndex 的值,所以影响 changeEvent.newIndex 的根本因素就是 slotschildren

  找到了根本因素接下来就简单了。我重复执行出现问题的操作步骤,然后在这个过程中打印 slotschildren,我惊讶的发现当我向 Vue.Draggable 第一次拖入输入框组件时,slotschildren 的长度居然不一样!children 是空的, 而 slots 的长度虽然正常,但其中的虚拟节点的 elm 属性却是 undefined

  看到这里我恍然大悟,正是 slotschildren 异常的值导致了 changeEvent.newIndex 的计算错误,那么是什么导致了它们值的异常呢?也许你有注意到 slots 虽然长度正常,但其中的虚拟节点的 elm 属性却是 undefined

  是的,没错,正是因为输入框组件采用了懒加载的方式进行引入,而导致的这个诡异的问题!

  万万没想到,组件的引入方式居然还会导致奇怪的问题出现!

总结

  所以,如果想在 Vue.Draggable 中使用自定义组件,那么千万不要使用懒加载的方式引入这些组件!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/168269.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

05预测识别-依托YOLO V8进行训练模型的识别——对视频中的目标进行跟踪统计

上文中详细介绍了如何对视频进行抽帧,并对帧的图像进行目标识别。但在日常工作中,我们也会遇到需要对目标进行跟踪统计的情况,比如我们需要连续统计某一类目标有多少个的时候,如果单纯从帧中抽取图像的话,系统将无法判断是否为同一目标,从而造成目标数量统计的重复,导致…

最新大麦订单生成器 大麦订单图一键生成

1、8.6全新版 本次更新了四种订单模板生成 多模板自由切换 2、在软件中输入生成的信息&#xff0c;这里输入的是商品信息&#xff0c;选择生成的商品图片&#xff0c;最后生成即可 新版大麦订单生成 四种模板图样式展示 这个样式图就是在大麦生成完的一个订单截图&#xff…

lv11 嵌入式开发 ARM体系结构理论基础(异常、微架构)4

1 异常概念 处理器在正常执行程序的过程中可能会遇到一些不正常的事件发生 这时处理器就要将当前的程序暂停下来转而去处理这个异常的事件 异常事件处理完成之后再返回到被异常打断的点继续执行程序 2 异常处理机制 不同的处理器对异常的处理的流程大体相似&#xff0c…

SSM图书管理系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 SSM 图书管理系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和 数据库&#xff0c;系统主要…

2023美团外卖商家销量

数据内容字段如下 外卖ID 外卖STR 外卖商家名称 地址 城市 省份 电话 纬度 经度 月销 起送价 评分 经营许可证 食品许可证 资源下载&#xff1a;https://download.csdn.net/download/WANJIAWEN1002/88444367?spm1001.2014.3001.5503

排序算法之-冒泡

顺序排序算法原理 从头开始遍历未排序数列&#xff0c;遍历时比较相邻的两个元素&#xff0c;前面的大于后面的&#xff0c;则双方交换位置&#xff0c;一直比较到末尾&#xff0c;这样最大的元素会出现在末尾&#xff0c;接着再依次从头开始遍历剩余未排序的元素&#xff0c;…

Python自动化测试selenium指定截图文件名方法

这篇文章主要介绍了Python自动化测试selenium指定截图文件名方法&#xff0c;Selenium 支持 Web 浏览器的自动化&#xff0c;它提供一套测试函数&#xff0c;用于支持 Web 自动化测试&#xff0c;下文基于python实现指定截图文件名方法&#xff0c;需要的小伙伴可以参考一下 前…

drawio连接线使用技巧和功能大全

drawio连接线使用技巧和功能大全 drawio是一款强大的图表绘制软件&#xff0c;支持在线云端版本以及windows, macOS, linux安装版。 如果想在线直接使用&#xff0c;则直接输入网址draw.io或者使用drawon(桌案), drawon.cn内部完整的集成了drawio的所有功能&#xff0c;并实现了…

使用Redis实现文章阅读量、收藏、点赞数量记录功能

目录 一、前言二、业务分析三、Redis数据结构选择分析和实现3.1、三个数据缓存都分别使用 字符串 结构计数器存储对应数量值3.2、三个数据缓存使用一个 Hash 结构存储3.3、阅读量使用字符串结构计算器&#xff0c;收藏和点赞分别使用 Set 集合存储 四、总结 一、前言 在博客中会…

排序算法的空间复杂度和时间复杂度

一、排序算法的时间复杂度和空间复杂度 排序算法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度 稳定性 冒泡排序 O(n) O(n) O(n) O(1) 稳定 直接选择排序 O(n) O(n) O(n) O(1) 不稳定 直接插入排序 O(n) O(n) O(n) O(1) 稳定 快速排序 O(n…

《研发效能(DevOps)工程师》课程简介(五)丨IDCF

由国家工业和信息化部教育与考试中心颁发的职业技术证书&#xff0c;也是国内首个研发效能&#xff08;DevOps&#xff09;职业技术认证&#xff0c;内涵1000页学习教材2000分钟的课程内容讲解460多个技术知识点300多道练习题。 在这里&#xff0c;你不仅可以了解到华为、微软、…

在新的服务器上成功安装mysqlclient的方法【解决No matching distribution found for mysqlclient的问题】

前言&#xff1a;在某台Centos服务器上安装mysqlclient时一直报下面的错&#xff1a; WARNING: Discarding https://mirrors.aliyun.com/pypi/packages/6a/91/bdfe808fb5dc99a5f65833b370818161b77ef6d1e19b488e4c146ab615aa/mysqlclient-1.3.0.tar.gz#sha25606eb5664e3738b28…