VUE Draggable拖拽组件拖拽失效问题
引入问题
在使用vue拖拽组件时,遇到了一个非常反常规的问题。
首先我使用了reactive
包装了一个数组,数组中是对象数据,然后将这个reactive
响应式对象传递给拖拽组件,希望能通过这个组件实现一系列拖拽业务功能。
const myData = reactive([
{"aaa":123,"id":1},
{"aaa":234,"id":2}
]);
<draggable v-model="myData" element="'ul'" itemKey="element=>element.id">
<template #item="{element,index}">
<div>{{element.aaa}}</div>
</template>
</draggable>
感觉是没什么问题,数据也能正常加载出来,但是在拖拽时发现,能拖的动,但是放手后数据会回到原来的位置。
经过搜索,发现解决这个问题的方法很简单,就是 把reactive
换成ref
就可以了。
但是这个原因却让人无法理解,非常的违反思维常规。
在学习vue的时候,就经常听到一句话“使用普通变量时,建议用ref
,而使用对象或数据时要使用reactive
”,这也是因为ref只能对浅层数据变化触发响应式更新,例如const a = ref([])
,当使用a.value.push(123)
等方法修改列表数据时,watch
是监听不到这个改变的,或者说列表或对象元素内容的变化是无法触发响应式更新的,而也就是建议使用reactive
的原因,因为reactive
能对深层数据变化进行响应更新。
ref和reactive机制差异问题
关于这个问题,在使用搜索引擎、gpt等工具查找搜索后,发现了一种有意思的解释。这个解释说到“ref和reactive的数据更新和获取方式有差异,ref使用 Proxy 实现,可以检测更广泛的操作,如 .value 赋值等。而reactive,则是使用getter/setter实现,无法通过.value进行赋值操作”
所以其实可以猜测,在使用reactive时,可能是数据变更时无法使用.value进行数据修改,导致赋值失败,从而无法顺利进行新数据的渲染。
拖拽组件返回的数据问题
我们姑且认为上面这个说法是正确的,但是这又引发了一个问题。如果是因为某些机制差异,必须要使用ref,那么ref只能响应到浅层变化或引用发生变化,它又是如何能够在列表某个局部数据发生顺序变化时发生响应呢,我们修改局部数据,按说应该是属于深层数据修改。
那么很有可能,拖拽组件基于这个因素,其实 并没有在原数据上做修改,而是返回了一个新的数组,这样一来也就使得引用发生变更,触发了响应式更新
经过阅读拖拽组件的源代码,也确实是这样的:
onDragUpdate(evt) {
removeNode(evt.item);
insertNodeAt(evt.from, evt.item, evt.oldIndex);
const oldIndex = this.context.index; // 获取原来的位置索引
const newIndex = this.getVmIndex(evt.newIndex); // 获取新的位置索引
this.updatePosition(oldIndex, newIndex); // 触发列表更新操作
const moved = { element: this.context.element, oldIndex, newIndex };
this.emitChanges({ moved });
},
updatePosition(oldIndex, newIndex) {
// 这个updatePosition是一个箭头函数
// 这个函数会根据原数组、元素原始位置、新位置进行调整,并返回该调整后的数组
const updatePosition = list =>
list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
// 这里使用了list.splice(newIndex, 0, newElement)的方式插入数据,第二个参数为0表示插入模式
// 首先list.splice(oldIndex, 1)会删除旧位置元素内容
// 返回该数据会,通过list.splice(oldIndex, 1)[0]可以获取到该元素
// list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);返回了这个数组的更新顺序后的呢绒
this.alterList(updatePosition); //这里是直接传了一个函数到alterList函数中
},
alterList(onList) {
// 这里的onList就是updatePosition(list),注意list是一个函数参数
// 这个函数中会对dragable的数据传入方式进行判断
// 如果传入dragable的是list,那么会直接在这个list上修改并返回
if (this.list) {
onList(this.list);
return;
}
// 而如果传入的是value,则会先展开value,创建一个list,修改后并返回
// 根据官方文档中对value和list参数的描述,在使用v-model时是value而非list
const newList = [...this.value];
onList(newList);
this.$emit("input", newList);
},
list: {
type: Array,
required: false,
default: null
},
value: {
type: Array,
required: false,
default: null
},
/*
使用v-model时,是用的value而非list:
value
Type: Array
Required: false
Default: null
Input array to draggable component. Typically same array as referenced by inner element v-for directive.
This is the preferred way to use Vue.draggable as it is compatible with Vuex.
It should not be used directly but only though the v-model directive:
<draggable v-model="myArray">
list
Type: Array
Required: false
Default: null
Alternative to the value prop, list is an array to be synchronized with drag-and-drop.
The main difference is that list prop is updated by draggable component using splice method, whereas value is immutable.
Do not use in conjunction with value prop.
*/
通过上述代码分析我们可以得知
(1) 在使用v-model传入数据到dragable组件时,数据将传入到value而非list属性中
(2) 这时在拖拽发生数据改变时,沿着其函数调用栈,会发现最终其实是创建了一个新的listconst newList = [...this.value]
,并通过onList(newList);
在这个新的list上修改了数据的顺序
(3) 最终会通过this.$emit("input", newList);
返回结果给父组件。
因此父组件接收到的其实是一个新的list数据,因此在使用ref时,相当于是修改了数据的引用,从而能够顺利触发响应式变化。
至于是否是由于“reactive通过set来设置数据,而ref是通过.value来设置数据,从而导致reactive无法正常使用”的问题,通过源代码可以看到这个新的列表newList,会通过一个名称为input的事件往外抛给父组件,由父组件进行处理。父组件中处理的这部分源代码我暂时没精力去阅读,有兴趣的可以去看一看
转载自:https://juejin.cn/post/7242129428510883896