在 Vuex 中,同时具有“动作”和“突变”的逻辑是什么?
我理解组件无法修改状态的逻辑(这似乎很聪明),但同时具有动作和突变似乎你正在编写一个函数来触发另一个函数,然后改变状态。
“动作”和“突变”之间有什么区别,它们如何协同工作,而且我很好奇为什么 Vuex 开发人员决定这样做?
mutations
和 actions
在 vuex 文档中都被定义为更改状态的方法。您无需执行任何操作即可提交突变。
问题 1:为什么 Vuejs 开发人员决定这样做?
回答:
当你的应用程序变大,并且有多个开发人员在这个项目上工作时,你会发现“状态管理”(尤其是“全局状态”)会变得越来越复杂。 vuex 方式(就像 react.js 中的 Redux 一样)提供了一种新的机制来管理状态、保持状态和“保存和可跟踪”(这意味着每个修改状态的操作都可以被调试工具跟踪:vue-devtools)
问题2:“动作”和“变异”有什么区别?
先来看看官方的解释:
突变:Vuex 突变本质上是事件:每个突变都有一个名称和一个处理程序。 import Vuex from 'vuex' const store = new Vuex.Store({ state: { count: 1 }, mutation: { INCREMENT (state) { // mutate state state.count++ } } }) 动作:动作只是调度的函数突变。 // 最简单的动作函数 increment ({commit}) { commit('INCREMENT') } // 带有附加参数的动作 // 带有 ES2015 参数的解构函数 incrementBy ({ dispatch }, amount) { dispatch('INCREMENT', amount ) }
以下是我对上述内容的解释:
突变是修改状态的唯一方法
突变不关心业务逻辑,它只关心“状态”
动作是业务逻辑
action 一次可以提交多个突变,它只是实现业务逻辑,它不关心数据更改(通过突变管理)
突变是同步的,而动作可以是异步的。
换句话说:如果您的操作是同步的,则不需要操作,否则实现它们。
我相信,了解 Mutations 和 Actions 背后的动机可以让人们更好地判断何时使用哪些以及如何使用。在“规则”变得模糊的情况下,它还使程序员摆脱了不确定性的负担。在对它们各自的目的进行了一些推理之后,我得出的结论是,尽管使用 Actions 和 Mutations 的方法肯定是错误的,但我认为没有规范的方法。
让我们首先尝试理解为什么我们甚至会经历突变或动作。
为什么首先要通过样板文件?为什么不直接在组件中更改状态?
严格来说,您可以直接从您的组件中更改 state
。 state
只是一个 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)
}
...
我认为如果你用样板替换一些短的东西,你会希望样板也很小。因此,我认为突变是对状态的本地操作的非常薄的包装器,几乎没有业务逻辑。换句话说,mutations 主要用于像 setter 一样。
现在您已经集中了您的变更,您可以更好地了解您的状态更改,并且由于您的工具(vue-devtools)也知道该位置,它使调试更容易。还值得记住的是,许多 Vuex 的插件不直接观察状态来跟踪变化,而是依赖于突变。因此,对状态的“越界”更改对他们来说是不可见的。
那么突变,动作有什么区别呢?
动作(如突变)也驻留在商店的模块中,并且可以接收 state
对象。这意味着他们可以也直接对其进行变异。那么两者兼得有什么意义呢?如果我们认为突变必须保持小而简单,这意味着我们需要一种替代方法来容纳更复杂的业务逻辑。行动是做到这一点的手段。而且正如我们之前建立的那样,vue-devtools 和插件通过 Mutations 知道变化,为了保持一致,我们应该继续在我们的操作中使用 Mutations。此外,由于动作意味着包罗万象,并且它们封装的逻辑可能是异步的,因此动作也可以从一开始就简单地设为异步是有道理的。
人们经常强调动作可以是异步的,而突变通常不是。您可能决定将这种区别视为一种指示,即突变应该用于任何同步(以及任何异步操作);但是,如果您需要提交多个突变(同步),或者如果您需要使用突变中的 Getter,您会遇到一些困难,因为突变函数既不接收 Getter 也不接收 Mutations 作为参数......
...引出了一个有趣的问题。
为什么 Mutations 不接收 Getter?
这个问题我还没有找到满意的答案。我已经看到核心团队的一些解释,我认为充其量是没有实际意义的。如果我总结一下它们的用法,Getter 是用来计算(并且通常是缓存)状态的扩展。换句话说,它们基本上仍然是状态,尽管需要一些前期计算并且它们通常是只读的。至少这是鼓励使用它们的方式。
因此,防止 Mutations 直接访问 Getter 意味着现在需要三件事之一,如果我们需要从前者访问后者提供的一些功能:(1) Getter 提供的状态计算在可访问的某个地方复制到 Mutation(难闻的气味),或 (2) 计算值(或相关的 Getter 本身)作为显式参数传递给 Mutation(时髦),或 (3) Getter 的逻辑本身直接在 Mutation 中复制,而没有 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 中存在的一些代码显然是从 Mutation 的内部逻辑中渗出的。
在我看来,这是妥协的迹象。我相信允许 Mutations 自动接收 Getters 会带来一些挑战。它可以是 Vuex 本身的设计,也可以是工具(vue-devtools 等),或者是为了保持一些向后兼容性,或者是所有所述可能性的某种组合。
我不相信自己将 Getter 传递给您的 Mutations 必然表明您做错了什么。我认为它只是“修补”框架的缺点之一。
computed
输出。它们是只读的。查看突变的更好方法是删除您拥有的 if else
。 vuex 文档说你可以在一个动作中容纳超过 1 个 commit
。因此,假设您可以根据逻辑进行某些突变是合乎逻辑的。我认为行动是一种决定触发哪种突变的方式。
我认为 TLDR 的答案是突变是同步/事务性的。因此,如果您需要运行 Ajax 调用或执行任何其他异步代码,则需要在 Action 中执行此操作,然后提交一个变更,以设置新状态。
动作和突变之间的主要区别:
在突变中,您可以更改状态,但不能更改操作。在操作内部,您可以运行异步代码,但不能在突变中运行。在动作内部,您可以访问 getter、状态、突变(提交它们)、动作(调度它们)等,在突变中您只能访问状态。
我已经专业使用 Vuex 大约 3 年了,这就是我认为我已经弄清楚了动作和突变之间的本质区别,如何从一起使用它们中受益,以及如果你可以让你的生活变得更艰难不要很好地使用它。
Vuex 的主要目标是提供一种新模式来控制应用程序的行为:反应性。这个想法是将应用程序状态的编排卸载到一个专门的对象:存储。它方便地提供了将您的组件直接连接到您的商店数据的方法,以便在他们自己方便时使用。这允许您的组件专注于他们的工作:定义模板、样式和基本组件行为以呈现给您的用户。同时,商店处理繁重的数据负载。
不过,这不仅仅是这种模式的唯一优势。存储是整个应用程序的单一数据源这一事实提供了跨许多组件重用此数据的巨大潜力。这不是第一个尝试解决跨组件通信问题的模式,但它的亮点在于它通过基本上禁止组件修改此共享数据的状态来强制您对应用程序实施非常安全的行为,并强制它改为使用“公共端点”来请求更改。
基本思想是这样的:
store 有一个内部状态,组件永远不应该直接访问它(mapState 被有效禁止)
商店有突变,这是对内部状态的同步修改。突变的唯一工作是修改状态。只能从动作中调用它们。它们应该被命名来描述状态(ORDER_CANCELED,ORDER_CREATED)发生的事情。让它们简短而甜蜜。您可以使用 Vue Devtools 浏览器扩展来逐步完成它们(它也非常适合调试!)
商店也有动作,应该是异步的或返回一个承诺。它们是您的组件在想要修改应用程序状态时将调用的操作。它们应该以面向业务的操作(动词,即cancelOrder、createOrder)命名。这是您验证和发送请求的地方。如果需要更改状态,每个操作可能会在不同的步骤调用不同的提交。
最后,store 有 getter,你可以用它来向你的组件公开你的状态。随着应用程序的扩展,预计它们会在许多组件中大量使用。 Vuex 大量缓存 getter 以避免无用的计算周期(只要您不向 getter 添加参数 - 尽量不要使用参数),所以不要犹豫,广泛使用它们。只需确保您给出的名称尽可能接近地描述应用程序当前所处的状态。
话虽如此,当我们开始以这种方式设计应用程序时,魔法就开始了。例如:
我们有一个组件向用户提供订单列表,并可以删除这些订单
组件映射了一个 store getter (deletableOrders),它是一个带有 id 的对象数组
该组件在每一行订单上都有一个按钮,它的点击被映射到一个商店操作(deleteOrder),该操作将订单对象传递给它(我们将记住,它来自商店的列表本身)
store deleteOrder 操作执行以下操作:它验证删除它存储要临时删除的订单它提交 ORDER_DELETED 突变以及它发送 API 调用以实际删除订单的顺序(是的,在修改状态之后!)它等待调用结束(状态已经更新)并且在失败时,我们使用我们之前保持的顺序调用 ORDER_DELETE_FAILED 突变。
它验证删除
它存储临时删除的订单
它按顺序提交 ORDER_DELETED 突变
它发送 API 调用以实际删除订单(是的,在修改状态之后!)
它等待调用结束(状态已经更新)并且在失败时,我们使用我们之前保留的顺序调用 ORDER_DELETE_FAILED 突变。
ORDER_DELETED 突变将简单地从可删除订单列表中删除给定订单(这将更新 getter)
ORDER_DELETE_FAILED 突变只是将其放回原处,并修改为状态以通知错误(另一个组件错误通知将跟踪该状态以知道何时显示自身)
最后,我们获得了一种被视为“反应性”的用户体验。从我们用户的角度来看,该项目已被立即删除。大多数时候,我们希望我们的端点能够正常工作,所以这是完美的。当它失败时,我们仍然可以控制应用程序的反应方式,因为我们已经成功地将前端应用程序状态的关注点与实际数据分离。
请注意,您并不总是需要商店。如果您发现您正在编写如下所示的商店:
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
}
}
在我看来,您似乎只是将存储用作数据存储,并且可能错过了它的反应性方面,因为不让它也控制您的应用程序反应的变量。基本上,您可以并且应该将您的组件中编写的一些代码行卸载到您的商店。
根据 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.
}
}
})
Action handlers(increment) 接收一个 context 对象,它在 store 实例上公开相同的方法/属性集,因此您可以调用 context.commit 提交突变,或通过 context.state 和 context.getters 访问状态和 getter
突变:
Can update the state. (Having the Authorization to change the state).
行动:
Actions are used to tell "which mutation should be triggered"
以 Redux 方式
突变是减速器动作是动作
为什么两者都有??
当应用越来越大时,代码和行数会越来越多,那个时候你必须处理 Actions 中的逻辑而不是突变,因为突变是改变状态的唯一权限,它应该尽可能干净。
免责声明 - 我才刚刚开始使用 vuejs,所以这只是我对设计意图的推断。
时间机器调试使用状态快照,并显示动作和突变的时间线。理论上,我们可以只使用 actions
以及状态设置器和获取器的记录来同步描述突变。但是之后:
我们会有不纯的输入(异步结果),这会导致 setter 和 getter。这在逻辑上很难遵循,并且不同的异步 setter 和 getter 可能会令人惊讶地交互。突变事务仍然会发生这种情况,但是我们可以说事务需要改进,而不是作为操作中的竞争条件。动作中的匿名突变更容易重新出现这类错误,因为异步编程是脆弱且困难的。
事务日志将难以阅读,因为状态更改没有名称。它会更像代码而不是英语,缺少突变的逻辑分组。
与现在同步定义的差异点(在突变函数调用之前和之后)相比,记录数据对象上的任何突变可能会更棘手且性能更低。我不确定这是一个多大的问题。
将以下事务日志与命名突变进行比较。
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 无法知道回调何时实际调用 - 回调中执行的任何状态突变基本上是不可追踪的!
这也让我很困惑,所以我做了一个简单的演示。
组件.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;
在研究了这一点之后,我得出的结论是,突变是一种只专注于更改数据的约定,以更好地分离关注点并改进更新数据前后的日志记录。而动作是处理更高级别逻辑然后适当调用突变的抽象层
1.从docs:
动作类似于突变,不同之处在于:动作不是改变状态,而是提交突变。动作可以包含任意异步操作。
Actions可以包含异步操作,但是mutation不能。
2.我们调用突变,我们可以直接改变状态。我们也可以在动作中通过这样的方式改变状态:
actions: {
increment (store) {
// do whatever ... then change the state
store.dispatch('MUTATION_NAME')
}
}
Actions 是为处理更多其他事情而设计的,我们可以在那里做很多事情(我们可以使用异步操作)然后通过在那里调度突变来改变状态。
因为没有突变就没有状态!提交时——以可预见的方式改变状态的一段逻辑被执行。突变是设置或改变状态的唯一方法(所以没有直接的改变!),而且——它们必须是同步的。这个解决方案驱动了一个非常重要的功能:突变正在登录到 devtools。这为您提供了很好的可读性和可预测性!
还有一件事——行动。正如人们所说的那样——行动会带来突变。所以他们不会改变商店,也没有必要同步。但是,他们可以管理额外的异步逻辑!
似乎没有必要为了调用 mutations
而额外添加一层 actions
,例如:
const actions = {
logout: ({ commit }) => {
commit("setToken", null);
}
};
const mutations = {
setToken: (state, token) => {
state.token = token;
}
};
所以如果调用 actions
调用 logout
,为什么不调用突变本身呢?
一个动作的整个想法是从一个动作内部调用多个突变,或者发出一个 Ajax 请求或任何你能想象到的异步逻辑。
我们最终可能会有发出多个网络请求并最终调用许多不同突变的操作。
因此,我们尝试将 Vuex.Store()
中的复杂性尽可能多地填充到 actions
中,这使我们的 mutations
、state
和 getters
更简洁明了,并符合使库的模块化像 Vue 和 React 一样流行。