【问题标题】:VueJS: Why parent components method unable to delete/destroy child's child (`vue2-dropzone`) component entirely?VueJS:为什么父组件方法无法完全删除/销毁子组件(`vue2-dropzone`)?
【发布时间】:2020-04-14 18:21:38
【问题描述】:

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

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

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

CodeSandBox Demo

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>

components\slider.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(实用程序)

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 (prop slideItem) 传递给vue-dropzone 组件(在 slide-template.vue 中),当 dropzone 挂载时,它会在 @vdropzone-mounted 自定义事件上触发 manuallyAddFiles(slideItem.zones)

async manuallyAddFiles() 从 API(托管在 heroku 上)获取图像,创建 (generate_ObjURLfromImageStream(url)) 图像的唯一 blob URL (blob:/),然后调用插件 API dropZone.manuallyAddFile() 加载图像进入相应的dropzone。

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

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

CodeSandBox Demo

我不确定是什么导致了这个问题,可能是由于 vue 的反应系统或 Vue's Array reactivity caveat 导致了这个问题。

谁能帮我理解和解决这个问题,如果可能的话,指出问题根源的原因。

非常感谢您的帮助。

谢谢,

【问题讨论】:

    标签: vue.js vuejs2 vue-reactivity vue-dropzone


    【解决方案1】:

    我想你可能误解了发生了什么:

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

    所以 VueJS 删除了最后一行,因为它可能是要求最少的计算,然后重新计算预期的状态。这有很多侧面案例(有时,本地状态不会重新计算)。要避免这种情况:按照文档中的建议,使用 :key 来跟踪对象的 id。来自文档:

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

    这种默认模式是高效的,但仅适用于您的列表渲染输出不依赖于子组件状态或临时 DOM 状态(例如表单输入值)的情况。

    要给 Vue 一个提示以便它可以跟踪每个节点的身份,从而重用和重新排序现有元素,您需要为每个项目提供唯一的 key 属性。 key 的理想值是每个项目的唯一 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>

    在这段代码中:

    我更正了 counterrow 从未增加的事实 我添加了一个 :key

    The documentation of :key

    【讨论】:

      【解决方案2】:

      你的意思是什么

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

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

      【讨论】:

        猜你喜欢
        • 2019-03-04
        • 1970-01-01
        • 2019-03-16
        • 1970-01-01
        • 2018-12-25
        • 2017-11-30
        • 2017-01-07
        • 2020-09-16
        相关资源
        最近更新 更多