Vuex - 不要在变异处理程序之外更改 Vuex 存储状态

44

我为什么会收到这个错误:

Error [vuex] Do not mutate vuex store state outside mutation handlers.

这是什么意思?

当我尝试在编辑输入文件中输入时,它就会发生。

pages/todos/index.vue

<template>
  <ul>
    <li v-for="todo in todos">
      <input type="checkbox" :checked="todo.done" v-on:change="toggle(todo)">
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
      <button class="destroy" v-on:click="remove(todo)">delete</button>

      <input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">

    </li>
    <li><input placeholder="What needs to be done?" autofocus v-model="todo" v-on:keyup.enter="add"></li>
  </ul>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  data () {
    return {
      todo: '',
      editedTodo: null
    }
  },
  head () {
    return {
      title: this.$route.params.slug || 'all',
      titleTemplate: 'Nuxt TodoMVC : %s todos'
    }
  },
  fetch ({ store }) {
    store.commit('todos/add', 'Hello World')
  },
  computed: {
    todos () {
      // console.log(this)
      return this.$store.state.todos.list
    }
  },
  methods: {
    add (e) {

      var value = this.todo && this.todo.trim()
      if (value) {
        this.$store.commit('todos/add', value)
        this.todo = ''
      }

    },
    toggle (todo) {
      this.$store.commit('todos/toggle', todo)
    },
    remove (todo) {
      this.$store.commit('todos/remove', todo)
    },

    doneEdit (todo) {
      this.editedTodo = null
      todo.text = todo.text.trim()
      if (!todo.text) {
        this.$store.commit('todos/remove', todo)
      }
    },
    cancelEdit (todo) {
      this.editedTodo = null
      todo.text = this.beforeEditCache
    },
  },
  directives: {
    'todo-focus' (el, binding) {
      if (binding.value) {
        el.focus()
      }
    }
  },
}
</script>

<style>
.done {
  text-decoration: line-through;
}
</style>

存储/todos.js

export const state = () => ({
  list: []
})

export const mutations = {
  add (state, text) {
    state.list.push({
      text: text,
      done: false
    })
  },
  remove (state, todo) {
    state.list.splice(state.list.indexOf(todo), 1)
  },
  toggle (state, todo) {
    todo.done = !todo.done
  }
}

你有任何想法可以解决这个问题吗?


你可以直接关闭 Vuex 的严格模式。 - Xhua
@Xhua,严格模式是有原因的(更好的实践),最好保持它以获得更清晰的代码。 - kissu
10个回答

44

在一个属于 Vuex 的状态上使用 v-model 可能会有些棘手。

你在这里使用了 v-model 来控制 todo.text

<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">

使用:value读取值,使用v-on:inputv-on:change执行一个方法,在明确定义的Vuex突变处理程序中执行突变。

此问题在这里处理:https://vuex.vuejs.org/zh/guide/forms.html


2
谢谢。我已经阅读了那个。但是这个例子使用 v-model 是很好的 - https://github.com/nuxt/todomvc/blob/master/pages/_slug.vue? - Run
我建议在vuex中的表单中使用带有v-model的数据。 - Ilyas karim

27

你好,我遇到了相同的问题,并通过使用以下方法之一来克隆我的对象来解决它:

{ ...obj} //spread syntax 
Object.assign({}, obj)
JSON.parse(JSON.stringify(obj))

我认为你需要替换代码中的这一部分。

computed: {
  todos () {
    // console.log(this)
    return this.$store.state.todos.list
  }
}

有了这个

computed: {
  todos () {
    // console.log(this)
    return {...this.$store.state.todos.list}
  }
}

我不能确定这是否是最佳方式,但希望这对其他遇到同样问题的人有所帮助。


可以将其显示出来,但要将值设置回去,仍然需要进行每个字段的更改(设置新值)。 - Osify

13
这个错误可能是因为您浅复制了一个对象。这意味着您尝试复制一个对象,但对象不是原始类型(如StringNumber),因此它是按引用而不是按值传递的。
在这里,您认为将一个对象克隆到另一个对象中,但实际上仍然引用旧对象。由于您更改了旧对象,因此出现了这个错误提示。

这是来自Vue3文档的GIF(在我们的情况下仍然相关)。
左边显示了一个对象(杯子)未被正确克隆>>按引用传递。
右边是正确克隆的>>按值传递。更改后者不会更改原始对象。


正确处理此错误的方法是使用lodash,以下是在Nuxt中高效地加载它的方法:

  • 安装lodash-es,例如:yarn add lodash-es,这是一个经过优化的可剪枝的lodash ES模块
  • 您还可能需要在nuxt.config.js中对其进行转译,具体操作如下:
build: {
  transpile: ['lodash-es'],
}
  • 像这样将其加载到您的.vue组件中
<script>
import { cloneDeep } from 'lodash-es'

...
const properlyClonedObject = cloneDeep(myDeeplyNestedObject)
...
</script>

几个关键点:

  • 建议使用lodash来代替JSON.parse(JSON.stringify(object)),因为它可以处理一些边缘情况
  • 由于这种设置,我们仅从 lodash 中加载小型函数而非整个库,因此在性能方面没有惩罚。
  • lodash具有许多经过充分测试的有用函数,在JS中(没有核心库)严重缺乏。
  • 更新: 如果您正在寻找深度复制的解决方案,则structuredClone也是本地化和相当高效的选择,无需使用Lodash。

7

如果您能使用lodash,就不会有什么问题。

computed: {
  ...mapState({
    todo: (state) => _.cloneDeep(state.todo)
  })
}

5

如果有人仍然被这个困扰着,我通过复制/克隆存储状态来使我的代码正常运行。

在你的情况下,尝试做这样的事情...

computed: {
  todos () {
    return [ ...this.$store.state.todos.list ]
  }
}

这基本上是一个“展开运算符”,它会创建一个 todos.list 数组的克隆。通过这样做,您不会直接改变状态的值,只需不要忘记提交(commit),这样您的突变(mutations)将保存在store中。


1
但是这个答案已经给出了:https://dev59.com/4FYO5IYBdhLWcg3wTPvk#65651645 - Vega

2
export default new Vuex.Store({
    ...
    strict: true
})

尝试注释"strict"


谢谢。这对我的情况起作用了。 - v3nM
谢谢!我为这个问题苦苦挣扎了好几个小时...现在终于解决了 :) - undefined

1
如果您正在使用Vuex模块,且模块的data属性是一个对象而不是返回对象的函数,并且您正在将此模块共享给多个Store,则可能会遇到此错误。
因此,应该这样写:
// In stores/YourModule.js
export default {
  state: { name: 'Foo' },
}

将其改为:

// In stores/YourModule.js
export default {
  state: () => {
    return { name: 'Foo' };
  },
}

这实际上在这里有记录:

有时候我们可能需要创建一个模块的多个实例,例如:

创建使用相同模块的多个存储(例如,在SSR (打开新窗口)中避免有状态单例,当runInNewContext选项为false或'once'时); 在同一个store中多次注册相同的模块。 如果我们使用纯对象来声明模块的状态,那么该状态对象将被引用共享,并在变异时导致跨存储/模块状态污染。

这实际上是Vue组件内部数据面临的完全相同问题。 因此,解决方案也相同 – 使用函数声明模块状态(2.3.0+支持):


0
如果你的数据是一个包含对象的数组。下面的代码片段是解决方案。
const toyData = await this.$store.dispatch(
    `user/fetchCoinToys`,
    payload
)
const msgList = toyData.msglist.map((data) => {
    return { ...data }
})

展开运算符只会对对象进行浅拷贝。 - kissu

0

我必须添加变异并调用它,而不是直接设置。

错误的:

someAction({state, rootState}) {
    state.someValue = true;
}

正确:

mutations: {
    ...
    setSomeValue(state, val) {
        state.someValue = val;
    },
    ...
}
...
someAction({state, commit, rootState}) {
    commit('setSomeValue', true);
}

-2

这可能不是你的情况,但如果有人正在使用 TypeScript 并且遇到了相同的问题,在你的方法或其他地方添加 this: any 作为第一个参数应该可以解决问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接