You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue中v-for项移除再添加后视图属性不更新问题

Vue 图片编辑撤销/重做功能绑定失效问题

问题场景

正在开发支持产品图片添加及Undo(撤销)、Redo(重做)功能的Vue特性,已简化代码复现问题:
存在ImageItem组件,通过v-for指令渲染,接收imageModel对象类型prop,内部通过textarea绑定并修改该对象的description属性。当列表项被移除后重新添加时,textarea的value属性绑定似乎失效。

复现步骤

  • 点击「Add image」按钮,添加带有textarea的行;
  • 在textarea中输入图片描述文本;
  • 点击「Undo」按钮,撤销描述修改(textarea变为空);
  • 再次点击「Undo」按钮,移除图片项;
  • 点击「Redo」按钮,恢复图片项(textarea仍为空,符合预期);
  • 再次点击「Redo」按钮,此时应恢复描述,但textarea未显示内容,绑定失效。

完整代码(与Vue Playground一致)

App.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);
    }
}

内容的提问来源于stack exchange,提问作者user2190492

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 02:38:11