Vuex行动与变异


173

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

我了解组件无法修改状态(看起来很聪明)的逻辑,但是同时具有动作和突变似乎就像您在编写一个函数来触发另一个功能,然后改变状态一样。

“动作”和“变异”之间有什么区别,它们如何协同工作,而且,我很好奇为什么Vuex开发人员决定采用这种方式?


2
我认为,请参见“行动起来”:vuex.vuejs.org/en/mutations.html#on-to-actions
Roy J


1
您不能直接更改商店的状态。更改商店状态的唯一方法是显式提交突变。为此,我们需要采取行动来实施突变。
Suresh Sapkota '18

1
@SureshSapkota这种说法十分混乱,因为这两个mutationsactions的vuex文档中definted为改变状态的方法。您不需要采取任何操作即可实施突变。
格雷厄姆

1
顾名思义,变异用于修改/变异您的状态对象。动作与突变非常相似,但是动作不会突变状态,而是执行突变。动作可以包含任何任意的异步代码或业务逻辑。Vuex建议仅在Mutation函数内部对状态对象进行突变。还建议不要在Mutation函数内运行任何繁重或阻塞的代码,因为它本质上是同步的
Emmanuel Neni

Answers:


221

问题1:为什么Vuejs开发人员决定采用这种方式?

回答:

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

问题2:“操作”和“变异”有什么区别?

首先让我们看一下官方的解释:

变异:

Vuex突变本质上是事件:每个突变都有一个名称和一个处理程序。

import Vuex from 'vuex'

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

动作:动作只是调度突变的函数。

// the simplest action
function increment (store) {
  store.dispatch('INCREMENT')
}

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

这是我对上述内容的解释:

  • 突变是修改状态的唯一方法
  • 突变不关心业务逻辑,它只关心“状态”
  • 行动是业务逻辑
  • 动作一次可以分配多个突变,它仅实现业务逻辑,不关心数据更改(通过突变进行管理)

80
动作“是业务逻辑”并且可以一次调度多个变异的事实很有帮助。这就是我一直在寻找的答案。谢谢。
科比

11
您暗示说您“派发了一个突变”。您提交突变的正确措辞不是吗?
ProblemsOfSumit

4
您分派动作并提交突变。
eirik

4
分派在vue 2.0中不再适用于突变,您需要在操作中进行突变。
SKLTFZ

18
@Kaicui该答案缺少有关突变始终是同步的且动作可能是异步的注释。除此之外,一个很好的答案!
decates

58

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

换句话说:如果您的操作是同步的,则不需要执行操作,否则可以执行操作。


2
这实际上回答了我要提出的有关todomvc示例如何不使用动作的问题。
sksallaj

7
“如果您的操作是同步的,则不需要操作”:事实并非如此:如果您想从同一个模块中编写多个变异,则确实需要操作,因为您不能从一个操作中调用另一个操作。
Raymundus

1
该答案的显而易见的后续行动将是“那为什么不仅仅采取行动并摆脱突变”
Michael Mrozek

34

我相信,了解“突变和动作”背后的动机可以使人们更好地判断何时使用哪种方式。在“规则”变得模糊的情况下,这也使程序员摆脱了不确定的负担。在对它们各自的用途进行了一些推理之后,我得出的结论是,尽管使用操作和变异的方法肯定是错误的,但我认为并没有一种规范的方法。

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

为什么首先要通过样板?为什么不直接在组件中更改状态?

严格来说,您可以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)
}
...

我认为,如果您用样板替换一些短的东西,您会希望样板也要小。因此,我认为突变是针对状态本机操作的非常薄的包装,几乎没有业务逻辑。换句话说,突变意味着像塞特犬一样经常使用。

既然您已经集中了突变,就可以更好地了解状态变化,并且由于您的工具(vue-devtools)也知道该位置,因此使调试更加容易。还需要记住的是,许多Vuex的插件并不直接监视状态来跟踪更改,而是依赖于突变。因此,对状态的“超出范围”更改对他们是不可见的。

那么mutationsactions到底有什么区别呢?

动作(如突变)也驻留在商店的模块中,并且可以接收state对象。这意味着,他们可能还直接发生变异它。那么,两者都具有什么意义呢?如果我们认为必须使变异保持小而简单,则意味着我们需要一种替代方法来容纳更复杂的业务逻辑。行动是做到这一点的手段。而且,由于我们早先已经建立了,vue-devtools和插件可以通过Mutations感知更改,因此,为了保持一致,我们应该继续从操作中使用Mutations。此外,由于动作本应包含所有内容,并且它们封装的逻辑可以是异步的,所以有意义的是,动作从一开始也将简单地变为异步的。

人们经常强调动作可以是异步的,而变异通常不是异步的。您可能会决定将这种区别视为一种指示,即对于同步的任何事物(对于异步的任何事物)都应使用突变;但是,例如,如果您需要(同步)提交多个突变,或者需要使用突变中的Getter,则您会遇到一些困难,因为突变函数既不接受Getter也不接受Mutations作为参数。

...导致一个有趣的问题。

为什么突变不接受吸气剂?

对于这个问题,我还没有找到满意的答案。我已经看到核心团队的一些解释,我认为这充其量是最好的。如果我总结一下它们的用法,则应该将Getter扩展为该状态的扩展(通常是缓存的)。换句话说,它们基本上仍然是状态,尽管需要一些预先计算,并且它们通常是只读的。至少这是鼓励它们被使用的方式。

因此,阻止Mutations直接访问Getters意味着,如果我们需要从前者访问后者提供的某些功能,则现在有三件事之一是必要的:(1)Getter提供的状态计算要么在可访问的某个地方重复进行,到Mutation(难闻的气味),或(2)将计算的值(或相关的Getter本身)作为显式变元传递给Mutation(笨拙),或(3)Getter的逻辑本身直接在Mutation中重复,而没有Getter(stench)提供的额外缓存优势。

下面是(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 
        }
    }
}

在我看来,以上内容似乎不仅令人费解,而且有些“漏水”,因为《行动》中的某些代码显然是从“变异”的内部逻辑中渗出的。

我认为,这表明存在妥协。我相信,允许Mutations自动接收Getters会带来一些挑战。它既可以是Vuex本身的设计,也可以是工具(vue-devtools等)的设计,或者是保持一定的向后兼容性,或者是所有陈述的可能性的某种组合。

我不相信自己将Getters传递给您的Mutations必然表明您做错了事。我认为这只是“修补”框架的缺点之一。


1
对我来说,这是最好的答案。只有阅读完之后,当您觉得自己了解某些内容时,我才会有这种“点击”的感觉。
罗伯特·库兹尼尔

吸气剂本质上是computed输出。它们是只读的。一种更好的查看突变的方法可能是删除if else已有的突变。vuex文档说您可以commit在一个动作中容纳多个。因此,假设您可以根据逻辑进行某些突变是合乎逻辑的。我将动作视为指示WHICH突变触发的一种方式。
Tamb

@Tamb:State和Getters都提供上下文数据。在决定如何修改国家之前会先询问他们,这是有道理的。当可以从状态中完全获取该信息时,将整个逻辑封装在单个Mutation中是有意义的,因为它可以访问状态。这是安装员的标准操作程序。不太有意义的是仅仅因为我们现在需要查询Getter以获取类似信息而采用根本不同的方法。
Michael Ekoka

@Tamb:您的建议是,当我们需要查询Getter时,我们应该更改上面的模式并将选择逻辑移到一个代理Action上,该Action可以访问Getter并可以将一堆微小的哑突变粘合在一起。它确实有效,但是它仍然circuit回曲折,无法解决我在回答中指的难闻气味,它只是将其移至其他位置。
Michael Ekoka

文档说在需要计算状态时使用吸气剂。因此,目前看来它们与计算属性相似。说动作可以将突变粘合在一起,就可以了解您的意思。文档明确表示要将业务逻辑放入动作中。
Tamb)

15

我认为TLDR的答案是,突变旨在实现同步/交易。因此,如果您需要运行Ajax调用或执行任何其他异步代码,则需要在Action中执行该操作,然后在之后进行更改以设置新状态。


1
这看起来像是文档的概述。这没有错。但是,此答案的问题在于它所断言的不一定是正确的。您可以在调用异步函数/ AJAX时在突变内修改状态,然后可以在完整的回调中对其进行更改。我认为这就是为什么在与Vuex一起工作时为什么应该将操作用于最佳开发实践的原因引起了极大的困惑。我知道,当我开始与Vuex合作时,这一定会让我感到困惑。
Erutan409 '18

8

动作和突变之间的主要区别:

  1. 在内部操作中,您可以运行异步代码,但不能运行突变。因此,请对异步代码使用动作,否则请使用变异。
  2. 在动作内部,您可以访问获取器,状态,突变(提交它们),动作(调度它们)以访问状态。因此,如果您只想访问状态,请使用突变,否则请使用操作。

5

根据 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.
    }
  }
})

动作处理程序(increment)收到一个上下文对象,该对象在商店实例上公开了相同的方法/属性集,因此您可以调用context.commit进行更改,或通过context.state和context.getters访问状态和获取方法。


1
是来自“变异”功能的可能调用,是来自vuejs组件的方法?
艾伯托·阿库尼亚(AlbertoAcuña),

@AlbertoAcuña我有一个相同的问题,因为当我尝试这样做时,会引发一个错误,即未定义局部突变。
Rutwick Gangurde

5

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

时间机器调试使用状态快照,并显示操作和变异的时间线。从理论上讲,我们可以只actions记录状态设置器和获取器以同步描述突变。但是之后:

  • 我们会有不纯的输入(异步结果),这会导致设置者和获取者。这在逻辑上很难遵循,并且不同的异步设置器和获取器可能会令人惊讶地进行交互。那仍然可能发生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无法知道何时将实际调用该回调-回调中执行的任何状态突变本质上是不可追踪的!


4

变异:

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中的逻辑而不是突变中的逻辑,因为突变是更改状态的唯一权限,因此应尽可能保持干净。


2

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

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;

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


0

1.从docs

动作类似于突变,不同之处在于:

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

这些动作可以包含异步操作,但该突变不能。

2.我们调用突变,我们可以直接改变状态。并且我们也可以像这样通过以下方式更改状态:

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

Actions是为处理更多其他事情而设计的,我们可以在其中执行许多操作(可以使用异步操作),然后通过在其中进行调度突变来更改状态。


0

因为没有状态就没有突变!提交后,将执行一条以可预见的方式更改状态的逻辑。突变是设置或更改状态的唯一方法(因此,没有直接的更改!),此外,它们必须是同步的。该解决方案提供了非常重要的功能:变异正在登录devtools。这为您提供了出色的可读性和可预测性!

还有一件事-行动。就像人们所说的那样,动作会引起突变。因此,它们不会更改存储,因此不需要同步它们。但是,他们可以管理额外的异步逻辑!


0

似乎没有必要再加上一层actions来调用mutations,例如:

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

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

因此,如果调用actions电话logout,为什么不叫突变本身?

动作的整个想法是从一个动作内部调用多个变异,或者发出Ajax请求或您可以想象的任何一种异步逻辑。

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

因此,我们尝试的东西尽可能多的复杂性,从我们Vuex.Store()尽可能在我们actions这离开我们mutationsstategetters清洁,简单,与这类模块化,使得像Vue的图书馆和应对流行的线下降。


0

我已经专业使用Vuex大约3年了,这就是我想出的关于动作和突变之间的本质区别,如何从它们一起使用中受益以及如何使生活变得更艰难的方法。不好用。

Vuex的主要目标是提供一种控制应用程序行为的新模式:反应性。这个想法是将应用程序状态的流程分配到一个专门的对象:商店。它方便地提供了将组件直接连接到商店数据以方便自己使用的方法。这使您的组件可以专注于其工作:定义模板,样式和基本组件行为以呈现给用户。同时,存储处理繁重的数据负载。

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

基本思想是这样的:

  • 该存储具有内部状态,该状态永远不能由组件直接访问(有效地禁止了mapState)
  • 存储具有突变,这些突变是对内部状态的同步修改。变异的唯一工作就是修改状态。仅应从操作中调用它们。应该命名它们以描述状态发生的事情(ORDER_CANCELED,ORDER_CREATED)。让它们简短而甜美。您可以使用Vue Devtools浏览器扩展逐步进行调试(这也非常适合调试!)
  • 商店也有动作,这些动作应该是异步的或返回承诺。它们是您的组件在想要修改应用程序状态时将调用的操作。它们应使用面向业务的动作(动词,即cancelOrder,createOrder)来命名。您可以在此处验证并发送请求。如果需要更改状态,则每个动作可以在不同的步骤调用不同的提交。
  • 最后,商店有吸气剂,您可以使用吸气剂将状态暴露给组件。随着应用程序的扩展,期望它们在许多组件中得到大量使用。Vuex会大量缓存吸气剂,以避免无用的计算周期(只要您不向吸气剂中添加参数-尽量不要使用参数),因此请不要犹豫而广泛地使用它们。只要确保您提供的名称尽可能描述应用程序当前处于什么状态即可。

就是说,魔术始于我们开始以这种方式设计应用程序时。例如:

  • 我们有一个为用户提供订单列表的组件,可以删除这些订单
  • 组件已映射商店获取器(deletableOrders),它是具有ID的对象的数组
  • 该组件在订单的每一行上都有一个按钮,其单击映射到商店操作(deleteOrder),该操作将订单对象传递给它(我们将记住,它来自商店列表本身)
  • 存储deleteOrder操作执行以下操作:
    • 它验证删除
    • 它存储了暂时删除的命令
    • 它按顺序提交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
  }
}

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.