Vuex行动vs突变

在Vuex中,同时拥有“动作”和“突变”的逻辑是什么?

我理解组件不能修改状态的逻辑(这看起来很聪明),但同时拥有动作和突变似乎是在编写一个函数来触发另一个函数,然后再改变状态。

“动作”和“突变”之间的区别是什么,它们是如何一起工作的,更重要的是,我很好奇Vuex开发人员为什么决定这样做?

75228 次浏览

问题1:为什么Vuejs的开发者决定这样做?

答:

  1. 当您的应用程序变得很大,并且有多个开发人员在这个项目上工作时,您会发现“状态管理”;(特别是“全球状态”)变得越来越复杂。
  2. Vuex的方式(就像在react.js中Redux一样)提供了一种新的机制来管理状态、保存状态和“保存和跟踪”;(这意味着修改状态的每个动作都可以被调试工具:vue-devtools跟踪)

问题2:“action”和“;和“mutation" ?

让我们先看看官方的解释:

基因突变:

Vuex突变本质上是事件:每个突变都有一个名称和一个 处理程序。< / p >
import Vuex from 'vuex'


const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
INCREMENT (state) {
// mutate state
state.count++
}
}
})

动作:动作只是分派突变的函数。

// the simplest action
function increment ({commit}) {
commit('INCREMENT')
}


// a action with additional arguments
// with ES2015 argument destructuring
function incrementBy ({ dispatch }, amount) {
dispatch('INCREMENT', amount)
}

以下是我对上述问题的解释:

  • 突变是要修改状态的的唯一途径
  • 突变并不关心业务逻辑,它只关心"状态"
  • 行动是业务逻辑
  • 行动可以一次提交多个突变,它只是实现业务逻辑,它不关心数据更改(这是通过突变管理的)

突变是同步的,而操作可以是异步的。

换句话说:如果您的操作是同步的,则不需要操作,否则就实现它们。

免责声明-我只是刚刚开始使用vuejs,所以这只是我推断的设计意图。

时间机器调试使用状态的快照,并显示动作和变化的时间轴。理论上,我们可以只使用actions和状态setter和getter的记录来同步描述突变。但之后:

  • 我们会有不纯的输入(异步结果),这会导致setter和getter。这在逻辑上很难遵循,不同的异步setter和getter可能会意外地相互作用。这仍然会发生在mutations事务中,但我们可以说事务需要改进,而不是在操作中成为竞态条件。动作中的匿名突变更容易重新出现这类错误,因为异步编程是脆弱和困难的。
  • 事务日志很难读取,因为状态更改没有名称。它将更像代码,更少的英语,缺少突变的逻辑分组。
  • 记录数据对象上的任何突变可能更棘手,性能也更差,而现在有同步定义的差异点——在突变函数调用之前和之后。我不知道这有多严重。

将下面的事务日志与命名的突变进行比较。

Action: FetchNewsStories
Mutation: SetFetchingNewsStories
Action: FetchNewsStories [continuation]
Mutation: DoneFetchingNewsStories([...])

使用没有命名突变的事务日志:

Action: FetchNewsStories
Mutation: state.isFetching = true;
Action: FetchNewsStories [continuation]
Mutation: state.isFetching = false;
Mutation: state.listOfStories = [...]

我希望您能从这个例子中推断出动作中的异步和匿名突变可能增加的复杂性。

https://vuex.vuejs.org/en/mutations.html

现在,假设我们正在调试应用程序,并查看devtool的突变日志。对于记录的每个突变,devtool都需要捕获状态的“之前”和“之后”快照。然而,上面的突变例子中的异步回调使这成为不可能:当突变被提交时,回调还没有被调用,devtool没有办法知道回调什么时候会被实际调用——在回调中执行的任何状态突变本质上是不可跟踪的!

我认为TLDR的答案是,突变意味着同步/事务性的。因此,如果您需要运行Ajax调用,或执行任何其他异步代码,则需要在Action中执行该操作,然后提交一个突变,以设置新状态。

根据docs

行动类似于突变,区别在于:

  • 而不是变异状态,行动 提交突变。
  • 行动可以包含任意的异步操作。

考虑下面的代码片段。

const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++               //Mutating the state. Must be synchronous
}
},
actions: {
increment (context) {
context.commit('increment') //Committing the mutations. Can be asynchronous.
}
}
})

动作处理程序(增量)接收一个上下文对象,该对象公开同一组的 方法/属性,以便您可以调用 commit来提交一个突变,或者访问状态和getter 通过上下文。State和context.getters

1.从文档:

动作类似于突变,区别在于:

  • 操作不会导致状态突变,而是导致突变。
  • 动作可以包含任意异步操作。

action可以包含异步操作,但是突变不能。

2.我们调用突变,就能直接改变状态。我们也可以这样改变状态:

actions: {
increment (store) {
// do whatever ... then change the state
store.commit('MUTATION_NAME')
}
}

action是为处理更多其他事情而设计的,我们可以在那里做很多事情(我们可以使用异步操作),然后通过调度突变来改变状态。

因为没有突变就没有状态!提交时——执行以可预见的方式改变状态的一段逻辑。突变是设置或改变状态的唯一方法(所以没有直接的变化!),而且它们必须是同步的。这个解决方案驱动了一个非常重要的功能:突变将登录到devtools。这为您提供了良好的可读性和可预测性!

还有一件事——行动。正如我们所说的,行为会导致突变。所以它们不会改变存储,也不需要这些是同步的。但是,他们可以管理一个额外的异步逻辑!

这也让我感到困惑,所以我做了一个简单的演示。

component.vue

<template>
<div id="app">
<h6>Logging with Action vs Mutation</h6>
<p>\{\{count}}</p>
<p>
<button @click="mutateCountWithAsyncDelay()">Mutate Count directly with delay</button>
</p>
<p>
<button @click="updateCountViaAsyncAction()">Update Count via action, but with delay</button>
</p>
<p>Note that when the mutation handles the asynchronous action, the "log" in console is broken.</p>
<p>When mutations are separated to only update data while the action handles the asynchronous business
logic, the log works the log works</p>
</div>
</template>


<script>


export default {
name: 'app',


methods: {


//WRONG
mutateCountWithAsyncDelay(){
this.$store.commit('mutateCountWithAsyncDelay');
},


//RIGHT
updateCountViaAsyncAction(){
this.$store.dispatch('updateCountAsync')
}
},


computed: {
count: function(){
return this.$store.state.count;
},
}


}
</script>

store.js

import 'es6-promise/auto'
import Vuex from 'vuex'
import Vue from 'vue';


Vue.use(Vuex);


const myStore = new Vuex.Store({
state: {
count: 0,
},
mutations: {


//The WRONG way
mutateCountWithAsyncDelay (state) {
var log1;
var log2;


//Capture Before Value
log1 = state.count;


//Simulate delay from a fetch or something
setTimeout(() => {
state.count++
}, 1000);


//Capture After Value
log2 = state.count;


//Async in mutation screws up the log
console.log(`Starting Count: ${log1}`); //NRHG
console.log(`Ending Count: ${log2}`); //NRHG
},


//The RIGHT way
mutateCount (state) {
var log1;
var log2;


//Capture Before Value
log1 = state.count;


//Mutation does nothing but update data
state.count++;


//Capture After Value
log2 = state.count;


//Changes logged correctly
console.log(`Starting Count: ${log1}`); //NRHG
console.log(`Ending Count: ${log2}`); //NRHG
}
},


actions: {


//This action performs its async work then commits the RIGHT mutation
updateCountAsync(context){
setTimeout(() => {
context.commit('mutateCount');
}, 1000);
}
},
});


export default myStore;

经过研究,我得出的结论是,突变是一种只关注于更改数据以更好地分离关注点和改进更新数据前后的日志记录的约定。而动作是一个抽象层,它处理更高层次的逻辑,然后适当地调用突变

基因突变:

Can update the state. (Having the Authorization to change the state).

行动:

Actions are used to tell "which mutation should be triggered"

用Redux的方式

Mutations are Reducers
Actions are Actions

为什么都是??

当应用程序增长时,代码和行数将会增加,这时你必须在Actions中处理逻辑,而不是在突变中,因为突变是改变状态的唯一权威,它应该尽可能干净。

我相信,理解了突变和动作背后的动机,可以让人更好地判断什么时候使用什么以及如何使用。它还将程序员从“规则”变得模糊的情况下的不确定性负担中解放出来。在对它们各自的目的进行了一番推理之后,我得出的结论是,尽管使用动作和突变的方式肯定是错误的,但我不认为存在一个规范的方法。

让我们首先尝试理解为什么我们要经历突变或动作。

为什么一开始要看样板文件?为什么不直接改变组件的状态?

严格地说,你可以直接从你的组件中更改statestate只是一个JavaScript对象,没有什么神奇的东西可以恢复你对它所做的更改。

// Yes, you can!
this.$store.state['products'].push(product)

然而,这样做会将状态突变分散到各个地方。您无法简单地打开包含状态的单个模块,并一眼就能看到可以对其应用何种操作。集中突变解决了这个问题,尽管要付出一些样板文件的代价。

// so we go from this
this.$store.state['products'].push(product)


// to this
this.$store.commit('addProduct', {product})


...
// and in store
addProduct(state, {product}){
state.products.push(product)
}
...

我认为如果你用样板代替一些短的东西,你会希望样板也小。因此,我假定突变是对状态的本地操作的非常薄的包装,几乎没有业务逻辑。换句话说,突变应该主要像setter一样使用。

现在您已经集中了您的突变,您可以更好地概述您的状态更改,并且由于您的工具(vue-devtools)也知道该位置,因此调试变得更容易。同样值得记住的是,许多Vuex的插件并不直接监视状态来跟踪变化,而是依赖于突变来跟踪变化。因此,对状态的“越界”更改对他们来说是不可见的。

mutationsactions有什么区别呢?

动作,像突变一样,也驻留在商店的模块中,可以接收state对象。这意味着他们可以也直接突变它。那么两者兼得有什么意义呢?如果我们推断突变必须保持小而简单,这意味着我们需要另一种方法来容纳更复杂的业务逻辑。行动是做到这一点的手段。正如我们之前所建立的,vue-devtools和插件可以通过突变来感知变化,为了保持一致性,我们应该继续从我们的操作中使用突变。此外,由于动作应该是包涵一切的,而且它们所封装的逻辑可能是异步的,所以动作从一开始就应该是异步的。

人们经常强调动作可以是异步的,而突变通常不是。您可能决定将这种区别视为一种指示,即突变应该用于任何同步的事情(而操作用于任何异步的事情);然而,如果你需要提交多个突变(同步),或者你需要从突变中获取一个Getter,你就会遇到一些困难,因为突变函数既不接收Getter参数,也不接收突变参数。

...这就引出了一个有趣的问题。

为什么突变不接收getter ?

对于这个问题,我还没有找到一个满意的答案。我看过核心团队的一些解释,但我认为这些解释充其量是毫无意义的。如果我总结它们的用法,getter是对状态的计算(通常是缓存)扩展。换句话说,它们基本上仍然是状态,尽管这需要一些前期计算,而且它们通常是只读的。至少这是他们被鼓励使用的方式。

因此,防止突变直接访问getter意味着现在需要做三件事中的一件,如果我们需要从前者访问后者提供的一些功能:(1)要么由Getter提供的状态计算被复制到突变(bad smell)可以访问的某个地方,要么(2)计算值(或相关的Getter本身)作为显式参数传递给突变(funky),或者(3)Getter的逻辑本身直接在突变中复制,而没有Getter提供的缓存的额外好处(恶臭)。

下面是(2)的一个例子,在我遇到的大多数情况下,这似乎是“最不坏”的选项。

state:{
shoppingCart: {
products: []
}
},


getters:{
hasProduct(state){
return function(product) { ... }
}
}


actions: {
addProduct({state, getters, commit, dispatch}, {product}){


// all kinds of business logic goes here


// then pull out some computed state
const hasProduct = getters.hasProduct(product)
// and pass it to the mutation
commit('addProduct', {product, hasProduct})
}
}


mutations: {
addProduct(state, {product, hasProduct}){
if (hasProduct){
// mutate the state one way
} else {
// mutate the state another way
}
}
}

对我来说,上面的内容不仅有点复杂,而且有些“漏洞百出”,因为Action中的一些代码显然是从突变的内部逻辑中渗出来的。

在我看来,这是一种妥协的迹象。我相信允许突变自动接收getter会带来一些挑战。它可以是Vuex本身的设计,也可以是工具(vue-devtools等),或者是维护一些向后兼容性,或者是所有所述可能性的某种组合。

我不相信的是,将getter传递给您自己的突变一定是您正在做错误的事情的标志。我认为这只是“修补”框架的一个缺点。

动作和突变之间的主要差异:

  1. 在突变中,你可以改变状态,但不能改变它的行为。
  2. 您可以在动作内部运行异步代码,但不能在突变中运行。
  3. 在动作内部,你可以访问getter,状态,突变(提交它们),动作(分派它们)等等,在突变中你只能访问状态。

似乎没有必要为了调用mutations而添加额外的actions层,例如:

const actions = {
logout: ({ commit }) => {
commit("setToken", null);
}
};


const mutations = {
setToken: (state, token) => {
state.token = token;
}
};

因此,如果调用actions会调用logout,为什么不调用突变本身呢?

动作的整个思想是从一个动作内部调用多个突变,或者发出Ajax请求或任何你能想到的异步逻辑。

我们最终可能会有发出多个网络请求的动作,并最终调用许多不同的突变。

因此,我们试图将尽可能多的复杂性从Vuex.Store()中塞入actions中,这使得我们的mutationsstategetters更干净、更直接,并符合使Vue和React等库流行的那种模块化。

我已经专业地使用Vuex大约3年了,以下是我认为我已经弄清楚的动作和突变之间的本质区别,如何从良好地使用它们中受益,以及如果使用不好,如何使您的生活更加困难。

Vuex的主要目标是提供一种新的模式来控制应用程序的行为:反应。其思想是将应用程序状态的编排卸载到一个专门的对象:存储。它方便地提供方法,将组件直接连接到存储数据,以便在方便时使用。这允许组件专注于它们的工作:定义模板、样式和基本组件行为,以呈现给用户。同时,存储处理大量的数据负载。

不过,这并不是这种模式的唯一优势。存储是整个应用程序的单一数据源,这一事实为跨许多组件重用此数据提供了巨大的潜力。这不是第一个试图解决跨组件通信问题的模式,但它的亮点在于,它迫使你在应用程序中实现一种非常安全的行为,基本上禁止你的组件修改共享数据的状态,并强制它使用“公共端点”。要零钱。

基本思想是这样的:

  • 存储有一个内部状态,不应该被组件直接访问(mapState被有效禁止)
  • 存储具有突变,这是对内部状态的同步修改。突变的唯一作用就是改变状态。它们应该只从操作中调用。它们应该被命名来描述发生在状态上的事情(ORDER_CANCELEDORDER_CREATED)。让它们简短而甜蜜。你可以通过使用Vue Devtools浏览器扩展来逐步完成它们(它也非常适合调试!)
  • 存储也有操作,这些操作应该是异步的或返回一个承诺。它们是组件想要修改应用程序状态时调用的操作。它们应该以面向业务的行动命名(动词,即cancelOrdercreateOrder)。这是验证和发送请求的地方。如果需要更改状态,每个操作可以在不同的步骤调用不同的提交。
  • 最后,存储区还有getter,用于向组件公开状态。随着应用程序的扩展,它们将在许多组件中大量使用。Vuex大量缓存getter以避免无用的计算周期(只要您没有向getter添加参数-尽量不要使用参数),因此不要犹豫广泛使用它们。只需要确保所给出的名称能够尽可能准确地描述应用程序当前的状态。

话虽如此,当我们开始以这种方式设计应用程序时,魔法就开始了。例如:

  • 我们有一个组件,它向用户提供一个订单列表,并允许用户删除这些订单
  • 组件已经映射了一个存储getter (deletableOrders),它是一个带有id的对象数组
  • 组件在每一行订单上都有一个按钮,它的单击被映射到一个存储操作(deleteOrder),该操作将订单对象传递给它(我们将记住,它来自商店的列表本身)
  • 存储deleteOrder动作执行以下操作:
    • 它验证删除
    • 它存储要临时删除的订单
    • 它会按照顺序提交ORDER_DELETED突变
    • 它发送API调用来实际删除订单(是的,在修改状态之后!)
    • 它等待调用结束(状态已经更新),如果调用失败,则按照之前保持的顺序调用ORDER_DELETE_FAILED突变。
  • ORDER_DELETED突变将简单地从可删除订单列表中删除给定的订单(这将更新getter)。
  • ORDER_DELETE_FAILED突变只是将其放回,并修改状态以通知错误(另一个组件error-notification将跟踪该状态以知道何时显示自身)

最后,我们有一个被认为是“反应性”的用户体验。从用户的角度来看,该项目已被立即删除。大多数时候,我们希望端点正常工作,所以这很完美。当它失败时,我们仍然可以控制应用程序如何反应,因为我们已经成功地将前端应用程序的状态与实际数据分离。

请注意,你并不总是需要商店。如果你发现你正在写这样的存储:

export default {
state: {
orders: []
},
mutations: {
ADD_ORDER (state, order) {
state.orders.push(order)
},
DELETE_ORDER (state, orderToDelete) {
state.orders = state.orders.filter(order => order.id !== orderToDelete.id)
}
},
actions: {
addOrder ({commit}, order) {
commit('ADD_ORDER', order)
},
deleteOrder ({commit}, order) {
commit('DELETE_ORDER', order)
}
},
getters: {
orders: state => state.orders
}
}

在我看来,您只是将存储用作数据存储,并且可能错过了它的反应性方面,因为没有让它也控制应用程序响应的变量。基本上,您可以也应该将组件中编写的一些代码卸载到存储中。