VueJS:为什么父组件方法无法完全删除/销毁子组件('vue2-dropzone')组件?



我正在 vuejs 中创建一个滑块,并使用 vue2-dropzone 插件进行文件上传,其中每张幻灯片 (slide-template.vue( 都有一个vue2-dropzone组件。

当应用程序加载时,图像文件会手动添加到从图像 API(托管在 heroku 上(查询的每个vue2-dropzone(manuallyAddFile插件 API(中

问题是当我删除第一张幻灯片时,从子 (slide-template.vue( 组件调用父级 (slider.vue( 方法removeSlideFn(作为道具传递给子级(第一张幻灯片被删除,但不完全是第一张幻灯片的拖放区图像不会被销毁并保留在 DOM 中,而是从 DOM 中删除slide2的图像(下一张幻灯片(从 DOM 中删除(请在代码沙盒演示中尝试一次以真正知道我是什么意思(。当我删除slide2slide3时不会发生这种情况,而只会在slide1上发生。

代码沙盒演示

App.vue

<template>
<div id="app">
<img width="15%" src="./assets/logo.png">
<slider />
</div>
</template>
<script>
import slider from "./components/slider";
export default {
name: "App",
components: {
slider
}
};
</script>

组件\滑块.vue (父(

<template>
<div>
<hooper ref="carousel" :style="hooperStyle" :settings="hooperSettings">
<slide :key="idx" :index="idx" v-for="(slideItem, idx) in slideList">
<slide-template
:slideItem="slideItem" 
:slideIDX="idx"
:removeSlideFn="removeCurrSlide" />
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
<hooper-pagination slot="hooper-addons"></hooper-pagination>
</hooper>
<div class="buttons has-addons is-centered is-inline-block">
<button class="button is-info" @click="slidePrev">PREV</button>
<button class="button is-info" @click="slideNext">NEXT</button>
</div>
</div>
</template>
<script>
import {
Hooper,
Slide,
Pagination as HooperPagination,
Navigation as HooperNavigation
} from "hooper";
import "hooper/dist/hooper.css";
import slideTemplate from "./slide-template.vue";
import { slideShowsRef } from "./utils.js";
export default {
data() {
return {
sliderRef: "SlideShow 1",
slideList: [],
hooperSettings: {
autoPlay: false,
centerMode: true,
progress: true
},
hooperStyle: {
height: "265px"
}
};
},
methods: {
slidePrev() {
this.$refs.carousel.slidePrev();
},
slideNext() {
this.$refs.carousel.slideNext();
},
//Removes slider identified by IDX
removeCurrSlide(idx) {
this.slideList.splice(idx, 1);
},
// Fetch data from firebase
getSliderData() {
let that = this;
let mySliderRef = slideShowsRef.child(this.sliderRef);
mySliderRef.once("value", snap => {
if (snap.val()) {
this.slideList = [];
snap.forEach(childSnapshot => {
that.slideList.push(childSnapshot.val());
});
}
});
}
},
watch: {
getSlider: {
handler: "getSliderData",
immediate: true
}
},
components: {
slideTemplate,
Hooper,
Slide,
HooperPagination,
HooperNavigation
}
};
</script>

components/slide-template.vue (child, with vue2-dropzone(

<template>
<div class="slide-wrapper">
<slideTitle :heading="slideItem.heading" />
<a class="button delete remove-curr-slide" @click="deleteCurrSlide(slideIDX)" ></a>

<vue2Dropzone
@vdropzone-file-added="fileWasAdded"
@vdropzone-thumbnail="thumbnail"
@vdropzone-mounted="manuallyAddFiles(slideItem.zones)"
:destroyDropzone="false"
:include-styling="false"
:ref="`dropZone${ slideIDX }`"
:id="`customDropZone${ slideIDX }`"
:options="dropzoneOptions">
</vue2Dropzone>
</div>
</template>
<script>
import slideTitle from "./slide-title.vue";
import vue2Dropzone from "@dkjain/vue2-dropzone";
import { generate_ObjURLfromImageStream, asyncForEach } from "./utils.js";
export default {
props: ["slideIDX", "slideItem", "removeSlideFn"],
data() {
return {
dropzoneOptions: {
url: "https://vuejs-slider-node-lokijs-api.herokuapp.com/imageUpload",
thumbnailWidth: 150,
autoProcessQueue: false,
maxFiles: 1,
maxFilesize: 2,
addRemoveLinks: true,
previewTemplate: this.template()
}
};
},
components: {
slideTitle,
vue2Dropzone
},
methods: {
template: function() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<img data-dz-thumbnail/>
</div>
<div class="dz-details">
<!-- <div class="dz-size"><span data-dz-size></span></div> -->

<!-- <div class="dz-filename"><span data-dz-name></span></div>  -->
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-close"></i></div>
</div>`;
},
thumbnail: function(file, dataUrl) {
var j, len, ref, thumbnailElement;
if (file.previewElement) {
file.previewElement.classList.remove("dz-file-preview");
ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]");
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j];
thumbnailElement.alt = file.name;
}
thumbnailElement.src = dataUrl;
return setTimeout(
(function(_this) {
return function() {
return file.previewElement.classList.add("dz-image-preview");
};
})(this),
1
);
}
},
// Drag & Drop Events
async manuallyAddFiles(zoneData) {
if (zoneData) {
let dropZone = `dropZone${this.slideIDX}`;
asyncForEach(zoneData, async fileInfo => {
var mockFile = {
size: fileInfo.size,
name: fileInfo.originalName || fileInfo.name,
type: fileInfo.type,
id: fileInfo.id,
childZoneId: fileInfo.childZoneId
};
let url = `https://vuejs-slider-node-lokijs-api.herokuapp.com/images/${
fileInfo.id
}`;
let objURL = await generate_ObjURLfromImageStream(url);
this.$refs[dropZone].manuallyAddFile(mockFile, objURL);
});
}
},
fileWasAdded(file) {
console.log("Successfully Loaded Files from Server");
},
deleteCurrSlide(idx) {
this.removeSlideFn(idx);
}
}
};
</script>
<style lang="scss">
.slide-wrapper {
position: relative;
}
[id^="customDropZone"] {
background-color: orange;
font-family: "Arial", sans-serif;
letter-spacing: 0.2px;
/* color: #777; */
transition: background-color 0.2s linear;
//   height: 200px;
padding: 40px;
}
[id^="customDropZone"] .dz-preview {
width: 160px;
display: inline-block;
}
[id^="customDropZone"] .dz-preview .dz-image {
width: 80px;
height: 80px;
margin-left: 40px;
margin-bottom: 10px;
}
[id^="customDropZone"] .dz-preview .dz-image > div {
width: inherit;
height: inherit;
//   border-radius: 50%;
background-size: contain;
}
[id^="customDropZone"] .dz-preview .dz-image > img {
width: 100%;
}
[id^="customDropZone"] .dz-preview .dz-details {
color: white;
transition: opacity 0.2s linear;
text-align: center;
}
[id^="customDropZone"] .dz-success-mark,
.dz-error-mark {
display: none;
}
.dz-size {
border: 2px solid blue;
}
#previews {
border: 2px solid red;
min-height: 50px;
z-index: 9999;
}
.button.delete.remove-curr-slide {
padding: 12px;
margin-top: 5px;
margin-left: 5px;
position: absolute;
right: 150px;
background-color: red;
}
</style>

slide-title.vue(不是那么重要(

<template>
<h2 contenteditable @blur="save"> {{ heading }} </h2>
</template>
<script>
export default {
props: ["heading"],
methods: {
save() {
this.$emit("onTitleUpdate", event.target.innerText.trim());
}
}
};
</script>

utils.js (utility(

export async function generate_ObjURLfromImageStream(url) {
return await fetch(url)
.then(response => {
return response.body;
})
.then(rs => {
const reader = rs.getReader();
return new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read();
// When no more data needs to be consumed, break the reading
if (done) {
break;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
}
// Close the stream
controller.close();
reader.releaseLock();
}
});
})
// Create a new response out of the stream
.then(rs => new Response(rs))
// Create an object URL for the response
.then(response => {
return response.blob();
})
.then(blob => {
// generate a objectURL (blob:url/<uuid> list)
return URL.createObjectURL(blob);
})
.catch(console.error);
}

从技术上讲,这就是应用程序的工作方式,slider.vue 从数据库 (firebase( 加载和获取数据并存储在数据数组slideList中,循环slideList并将每个 slideData(propslideItem(传递给 vue-dropzone 组件(在 slide-template.vue 中(,当 dropzone 挂载时,它会在@vdropzone-mounted自定义事件上触发manuallyAddFiles(slideItem.zones)

异步manuallyAddFiles()从 API(托管在 heroku 上(获取图像,为图像 (blob:/( 创建 (generate_ObjURLfromImageStream(url(( 唯一的 blob URL,然后调用插件 APIdropZone.manuallyAddFile()将图像加载到相应的放置区。

若要删除当前幻灯片,子项deleteCurrSlide()使用当前幻灯片的idx调用父级 (slider.vue(removeSlideFn(作为 prop 传递(方法。removeSlideFn使用splice删除相应数组 idxthis.slideList.splice(idx, 1)处的项目。

问题是当我删除第一张幻灯片时,第一张幻灯片被删除但不是完全删除,第一张幻灯片的拖放区图像没有被破坏并且仍然保留在 DOM 中,而是slide2的图像(下一张幻灯片(从 DOM 中删除。

代码沙盒演示

我不确定是什么导致了这个问题,可能是由于 vue 的反应性系统中的某些东西或 Vue 的数组反应性警告导致了这个问题。

任何人都可以帮助我理解和解决这个问题,如果可能的话,指出问题根源的原因。

非常感谢您的帮助。

谢谢

我想你可能错过了正在发生的事情:

在 VueJS 中有一个缓存方法,它允许重用生成的现有组件: - 你的每个对象在渲染时都被认为是平等的(在 DOM 级别(。

所以 VueJS 删除了最后一行,因为它可能是要求最少的计算,然后重新计算预期状态。这有很多附带情况(有时,不会重新计算本地状态(。要说明这一点:按照文档中的建议,使用 :key 跟踪对象的 ID。从文档中:

当 Vue 更新使用 v-for 渲染的元素列表时,默认情况下它使用"就地补丁"策略。如果数据项的顺序发生了变化,Vue 不会移动 DOM 元素以匹配项的顺序,而是就地修补每个元素,并确保它反映应该在该特定索引处呈现的内容。这类似于 Vue 1.x 中 track-by="$index" 的行为。

此默认模式是有效的,但仅适用于列表呈现输出不依赖于子组件状态或临时 DOM 状态(例如表单输入值(的情况。

为了给 Vue 一个提示,以便它可以跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每个项目提供一个唯一的键属性。键的理想值是每个项目的唯一 ID。这个特殊属性大致相当于 1.x 中的 track-by,但它的工作方式类似于属性,所以你需要使用 v-bind 将其绑定到动态值......

new Vue({
el: "#app",
data: {
counterrow: 1,
rows: [],
},
methods: {
addrow: function() {
this.counterrow += 1;
this.rows.push({
id: this.counterrow,
model: ""
});
},
removerows: function(index) {
this.rows.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<table>
<tr>
<td><input type="text" name="test1" /></td>
<td><button class="btn" @click="addrow">add row</button></td>
</tr>
<tr v-for="(row,index) in rows" :key="row.id">
<td><input type="text" name="test2" v-model="row.model" /></td>
<td><button class="btn" @click="removerows(index)">remove </button></td>
</tr>
</table>
</div>

在此代码中:

我更正了计数器行从未增加的事实 我添加了一个 :键

的文档 :key

你的意思是什么

问题是当我删除第一张幻灯片时,第一张幻灯片

被删除但不是完全删除,第一张幻灯片的拖放区图像不会被销毁并且仍然保留在 DOM 中,而是 slide2(下一张幻灯片(的图像从 DOM 中删除。

据我所知,这些元素不再在 DOM 中

最新更新