为什么在 v-for 中删除并添加回项目时,视图中的属性没有更新

问题描述 投票:0回答:1

我正在开发一项功能,用户可以添加产品图像,并且具有撤消/重做功能。我尽可能地减少它以显示我的问题。

我在 Vue Playground 中创建了一个关于我的问题的最小示例: 看这里

有一个名为

ImageItem
的组件,它在
v-for
范围内渲染。它获取一个包含对象的 prop
imageModel
,并在内部使用该
imageModel
属性来访问属性
description
并使用文本区域显示/更改它。当添加、删除和添加回列表中的项目时,似乎与文本区域的
value
属性的绑定由于某种原因被破坏。

在我的示例应用程序中尝试以下操作:

  1. 单击“添加图像”按钮。添加了一行带有文本区域的行。
  2. 在文本区域中输入一些文本(图像描述)。
  3. 单击“撤消”按钮。描述更改被撤消(文本区域变为空)。
  4. 再次单击“撤消”按钮。图像项目被删除(“添加”操作被撤消)。
  5. 到目前为止,行为符合预期。
  6. 现在,单击“重做”按钮。该项目将添加回原来的位置。包含描述的文本区域仍然是空的,因为在其中键入内容是一个单独的操作。仍然是预期的行为。
  7. 再次单击“重做”,文本区域中的描述现在应该出现,但事实并非如此。 这就是问题所在。

为了完整性,我在这个问题中包含了代码。但它与 Vue Playground 中的代码相同。

应用程序.vue

<script setup>
  import { ref } from 'vue'
  import ImageItem from './ImageItem.vue';
  import { ProductEditModel } from './ProductEditModel';
  const productEditModel = ref(new ProductEditModel());
  function addImage() { 
    productEditModel.value.addImage(); 
  }
  function handleChangeImageDescription(imageModel, description) { 
    productEditModel.value.changeImageDescription(imageModel, description);
  }
  function undo() { productEditModel.value.editHistory.undo(); }
  function redo() { productEditModel.value.editHistory.redo(); }
</script>

<template>
  <div v-for="productImage of productEditModel.images" :key="productImage.key">
    <ImageItem :imageModel="productImage" @changeImageDescription="handleChangeImageDescription"></ImageItem>
  </div>
  <button @click="undo">Undo</button>
  <button @click="redo">Redo</button>
  <button @click="addImage">Add image</button>
</template>

ProductImage.js

export class ProductImage {
  constructor()
  {
    this.key = Math.random();
    this.description = '';
  }
}

ImageItem.vue

<script setup>
import { defineProps, defineEmits } from 'vue';
const emit = defineEmits(['removeImage', 'changeImage', 'changeImageDescription']);
const props = defineProps({
    imageModel: {
        type: Object,
        required: true
    }
});
function handleDescriptionChange(description)
{
  emit('changeImageDescription', props.imageModel, description);
}
</script>

<template>
  <div class="image-item">
    <img src="" alt="Example image here..." />
    Description:
    <textarea :value="props.imageModel.description" rows="4"
      @change="(event) => handleDescriptionChange(event.target.value)" />
  </div>
</template>

<style>
.image-item {
  border: 1px solid #aaa;
  padding: 10px;
}
.image-item * {
  vertical-align: middle;
}
img {
  min-width: 100px;
  min-height: 100px;
  border: 1px solid #d0d0d0;
}
</style>

EditHistory.js

export class EditHistory
{
    constructor()
    {
        this.undoStack = [];
        this.redoStack = [];
    }

    do(action)
    {
        action.execute(); 
        this.undoStack.push(action);
        this.redoStack = [];
    }

    undo(count = 1)
    {
        count = Math.min(this.undoStack.length, count);
        for (let i = count; i > 0; i--)
        {
            const undoAction = this.undoStack.pop();
            undoAction.unExecute();
            this.redoStack.push(undoAction);
        }
    }

    redo(count = 1)
    {
        count = Math.min(this.redoStack.length, count);
        for (let i = count; i > 0; i--)
        {
            const redoAction = this.redoStack.pop();
            redoAction.execute();
            this.undoStack.push(redoAction);
        }
    }

    canUndo() 
    {
        return this.undoStack.length > 0;
    }

    canRedo() 
    {
        return this.redoStack.length > 0;
    }
}

ProductEditModel.js

import { AddImageAction } from "./AddImageAction";
import { ChangeImageDescriptionAction } from "./ChangeImageDescriptionAction";
import { EditHistory } from "./EditHistory";

export class ProductEditModel
{
  constructor()
  {
    this.images = []; // Array of ProductImage instances.
    this.editHistory = new EditHistory();
  }
  addImage()
  {
    this.editHistory.do(new AddImageAction(this));
  }
  changeImageDescription(imageModel, description)
  {
    this.editHistory.do(new ChangeImageDescriptionAction(this, imageModel, description));
  }
}

AddImageAction.js

import { ProductImage } from './ProductImage.js';
export class AddImageAction
{
    constructor(productEditModel) 
    {
        this.productEditModel = productEditModel;
        this.addedImageModel = null;
    }

    getDescription()
    {
        return "Afbeelding toevoegen";
    }

    execute()
    {
        this.addedImageModel = new ProductImage();
        this.productEditModel.images.push(this.addedImageModel);
    }

    unExecute()
    {
        const index = this.productEditModel.images.indexOf(this.addedImageModel);
        console.assert(index > -1);
        this.productEditModel.images.splice(index, 1);
    }
}

ChangeImageDescriptionAction.js

export class ChangeImageDescriptionAction
{
    constructor(productEditModel, productImageEditModel, description) 
    {
        this.productEditModel = productEditModel;
        this.productImageEditModel = productImageEditModel;
        this.description = description;

        this.originalDescription = this.productImageEditModel.description;
    }
    getDescription()
    {
        return "Afbeelding omschrijving veranderen";
    }
    execute()
    {
        this.productImageEditModel.description = this.description;
        console.log("Changed description to: " + this.description);
    }
    unExecute()
    {
        this.productImageEditModel.description = this.originalDescription;
        console.log("Changed description back to original: " + this.originalDescription);
    }
}
javascript vue.js binding vuejs3 vue-component
1个回答
0
投票

Vue 中的反应式类存在已知问题,这些问题限制了类的编写方式,但这里不适用。

this
并不总是所有类中的响应式对象,但它们对响应式数据进行操作,因此预计响应性不会出现问题。

问题在于历史记录存储类实例(这是可以通过不可变状态避免的设计限制),但它不会在重做时重用图像实例,因此

description
在旧实例上发生了更改。

修复方法是:

execute()
{
  if (!this.addedImageModel)
    this.addedImageModel = new ProductImage();
    ...
© www.soinside.com 2019 - 2024. All rights reserved.