【问题标题】:Why does v-model mutate Vuex state instead of componets's local data?为什么 v-model 会改变 Vuex 状态而不是组件的本地数据?
【发布时间】:2018-11-15 23:11:50
【问题描述】:

我做了我的第一个Vue项目,一切正常,所以我添加了Vuex(以后不会是多余的,为了兴趣我尝试了它),一切都还好,直到我启用了严格模式.事实证明,组件在突变之外改变了存储状态,但是我不想要它,并在带有 data() 的组件中创建了 nessesary 对象的本地副本。

我的意图是在父组件中创建一个本地对象,然后使用v-model 将其属性传递给子组件(我知道它是v-bindv-on:input 的事件驱动语法糖)以及当本地对象被更新(通过v-model 在子级内部),父组件的一个方法调度动作到存储。取而代之的是,由于外部变异,我收到了一条错误消息,而且它仅在第二个和后续输入事件中发生。

另外,如果我在 ProdutRow 组件观察器中替换这些行,它会起作用:

item: {
                handler(value) {
                    this.$store.dispatch('updateProduct', {
                        product: value,
                        index: this.index,
                    });
                },
                deep: true,
            }

使用:product: {...value},Object.assign({}, value),但商店操作中的相同代码不会:它会引发相同的错误。

data() 不会创建指定道具的副本吗?如果是这样,为什么 Object.assign 不能在商店中工作?

代码:

store.js

    import Vue from 'vue';
    import Vuex from 'vuex';
    import {ADD_PRODUCT, UPDATE_PRODUCT, DELETE_PRODUCT, UPDATE_FORM, UPDATE_PREPAYMENT_IN_PERCENT} from './mutation-types';
    import _ from 'lodash';

    Vue.use(Vuex);

    let id = 1;
    const product = {
        order: id++,
        name: '',
        size: '',
        content: '',
        price: 0,
        number: 1,
        discount: '',
        guarantee: 0,
        promotion: 0,
        location: '',
        sum: 0,
    };
    export default new Vuex.Store({
        strict: true,
        state: {
            products: [
                Object.assign({}, product),
            ],
            form: {
                prepaymentInPercent: 100,
            }

        },
        getters: {
            total(state) {
                return state.products.reduce(function (acc, cur, index, array) {
                    return array.length > 1 ? acc + cur.sum : cur.sum;
                }, 0);
            },
            rest(state, getters) {
                return getters.total - getters.prepaymentInRub;
            },
            prepaymentInRub(state, getters) {
                return getters.total * state.form.prepaymentInPercent / 100;
            }
        },
        mutations: {
            [ADD_PRODUCT](state, product) {
                state.products.push(product);
            },
            [UPDATE_PRODUCT](state, {product, index}) {
                state.products.splice(index, 1, product);
            },
            [DELETE_PRODUCT](state, index) {
                state.products.splice(index, 1);
            },
            [UPDATE_FORM](state, form) {
                state.form = form;
            },
            [UPDATE_PREPAYMENT_IN_PERCENT](state, percent) {
                state.form.prepaymentInPercent = percent;
            }

        },
        actions: {
            addProduct({commit}) {
                let newProduct = Object.assign({}, product);
                newProduct.order = id++;
                commit(ADD_PRODUCT, newProduct);
            },
            updateProduct: _.debounce(function ({commit}, product) {
                commit(UPDATE_PRODUCT, product);
            }, 1),
            deleteProduct({commit, state}, index) {
                state.products.length > 1 && commit(DELETE_PRODUCT, index)
            },
            updatePrepaymentInPercentByRub({commit, getters}, rubles) {
                let percent = Math.round(rubles / getters.total * 100);
                commit(UPDATE_PREPAYMENT_IN_PERCENT, percent);
            }
        },
    });

ProductTable.vue

    <template>
      <table border="0">
        <thead>
        <tr>
          <th class="pointer" @click="addProduct">+</th>
          <th>Номер</th>
          <th>Название</th>
          <th>Размер</th>
          <th>Наполнение</th>
          <th>Цена</th>
          <th>Количество</th>
          <th>Скидка</th>
          <th>Акция</th>
          <th>Сумма</th>
          <th>Гарантия</th>
          <th>Заказ</th>
          <th class="pointer" @click="toJSON">JSON</th>
        </tr>
        </thead>
        <tbody>
        <template v-for="(product, index) in products">
          <ProductRow
                  :initialItem="product"
                  :key="product.order"
                  :index="index"
          />
        </template>
        <tr>
          <td colspan="12">{{total}}</td>
          <td>{{json}}</td>
        </tr>
        </tbody>
      </table>
    </template>

    <script>
      import ProductRow from './ProductRow';
      import {mapGetters, mapActions, mapState} from 'vuex';

      export default {
        components: {
          ProductRow,
        },
        name: 'ProductTable',
        data() {
          return {
            json: '',
          };
        },
        computed: {
          ...mapState(['products']),
          ...mapGetters(['total']),
        },
        methods: {
          ...mapActions(['addProduct']),
          toJSON() {
            this.json = JSON.stringify({
              products: this.products,
              total: this.total,
            }, null, '\t');
          },
        },
      };
    </script>

ProductRow

<template>
    <tr>
        <td colspan="2" class="id">{{indexFrom1}}</td>
        <Editable v-model="item.name"/>
        <Editable v-model="item.size"/>
        <Editable v-model="item.content"/>
        <Editable v-model.number="item.price"/>
        <Editable v-model.number="item.number"/>
        <Editable v-model="item.discount"/>
        <td>
            <select v-model="item.promotion">
                <option selected="" value="0">Нет</option>
                <optgroup label="Новоселы">
                    <option data-text="Нов." value="5">Новоселы -5%</option>
                    <option data-text="Нов." value="10">Новоселы -10%</option>
                    <option data-text="Нов." value="15">Новоселы -15%</option>
                </optgroup>
            </select>
        </td>
        <td>{{sum}}</td>
        <Editable v-model.number="item.guarantee"/>
        <td>
            <select v-model="item.location">
                <option selected value="">Услуги</option>
                <option value="СКЛАД">Склад</option>
                <option value="ЗАКАЗ">Заказ</option>
            </select>
        </td>
        <td>
            <span class="table-remove" @click="removeProduct(index)">Удалить</span>
        </td>
    </tr>
</template>

<script>
    import Editable from './EditableCell';

    export default {
        components: {
            Editable,
        },
        name: 'ProductRow',
        props: {`enter code here`
            initialItem: Object,
            index: Number,
        },
        data() {
            return {
            item: this.initialItem
        };

        },
        computed: {
            sum() {
                let prod = this.item.price * this.item.number;
                let discounted = this.isDiscountInPercent(this.item.discount) ?
                    prod * this.getCoeffFromPercent(this.item.discount) :
                    prod - this.item.discount;
                let result = Math.round(discounted * this.getCoeffFromPercent(this.item.promotion));
                return result > 0 ? result : 0;
            },
            indexFrom1() {
                return this.index + 1;
            },
        },
        methods: {
            getCoeffFromPercent(percent) {
                return 1 - parseInt(percent) / 100;
            },
            isDiscountInPercent(discount) {
                return ~discount.indexOf('%') ? true : false;
            },
            removeProduct(index) {
                // console.log(arguments);
                this.$store.dispatch('deleteProduct', index)

            }
        },
        watch: {
            sum() {
                this.item.sum = this.sum;
            },
            item: {
                handler(value) {
                    this.$store.dispatch('updateProduct', {
                        product: value,
                        index: this.index,
                    });
                },
                deep: true,

            },
        },
    };
</script>

【问题讨论】:

    标签: vue.js vue-component vuex


    【解决方案1】:

    不,data() 不会创建项目对象的副本,因此在此代码中您通过引用传递对象。

    data() {
       return {
            item: this.initialItem
        };
    }
    

    这意味着您商店中的产品对象与您的 ProductRow 组件中的this.item 完全相同。因此,当您将v-model 附加到输入时,您将直接更改商店中的产品对象。

    在您的商店中使用 Object.assign() 克隆产品对象将不起作用。您必须在 ProductRow 组件中进行克隆。

    data() {
       return {
          item: Object.assign({}, this.initialItem)
       };
    }
    

    这将创建一个副本,这样您就不会直接修改商店中的产品。

    【讨论】:

    • 谢谢。我已经明白了。这就是为什么只有第一个事件可以正常工作,但是在该商店从 ProductRow 的data() 接收到对象之后,并且组件本身没有更新(data() 被调用一次),因此它们引用了同一个对象。我必须在观察者中创建一个新副本,然后通过动作调度将其传递给商店,或者不创建副本,在事件侦听器中传递值,然后也调度动作。我认为前者有点冗长(最初和后来使用 Object.assign),后者不能使用sum 计算属性。
    • 我不确定我是否理解您解释的两种方法。您只需在数据函数中调用一次Object.assign()。这将为您的组件创建项目对象的本地副本。无需调用Object.assign() 并在观察者中再次复制对象,因为项目对象的本地副本是使用v-model 编辑的对象。
    猜你喜欢
    • 2017-08-01
    • 1970-01-01
    • 2018-10-02
    • 2018-05-12
    • 2018-12-30
    • 1970-01-01
    • 2021-02-15
    • 2020-09-10
    • 1970-01-01
    相关资源
    最近更新 更多