Vuex动作与突变

时间:2016-09-02 18:53:22

标签: vue.js vuex

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

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

“行动”和“突变”之间有什么区别,它们如何协同工作,而且我很好奇为什么Vuex开发人员决定这样做?

13 个答案:

答案 0 :(得分:136)

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

答案:

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

    问题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)
    }
    

    以下是我对上述内容的解释:

    • 变异是修改状态的唯一方式
    • 变异并不关心业务逻辑,只关心"状态"
    • 操作是业务逻辑
    • 操作可以调度一次超过1个突变,它只是实现业务逻辑,它不关心数据更改(通过突变管理) )

答案 1 :(得分:42)

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

以另一种方式表达:如果您的操作是同步的,则不需要操作,否则实现它们。

答案 2 :(得分:13)

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

答案 3 :(得分:8)

我相信,了解“突变和动作”背后的动机可以使人们更好地判断何时使用哪种方式。在“规则”变得模糊的情况下,这也使程序员摆脱了不确定的负担。在尝试考虑了它们各自的目的之后,我得出结论:使用它们肯定有错误的方法,但是我不认为有一种规范的方法。

让我们从理解为什么我们甚至经历突变或动作开始。

  

为什么首先要经过锅炉房?为什么不直接在组件中更改状态?

严格来说,您可以直接从组件中更改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)
}
...

我认为,如果用样板替换一些短的东西,您会希望样板也要小。因此,我认为突变意味着在该州的本机操作周围是非常薄的包装,几乎没有业务逻辑。换句话说,突变通常像二传手一样使用。

现在,您已经集中了突变,您可以更好地了解状态变化,并且由于您的工具(vue-devtools)也知道该位置,因此可以简化调试。还需要记住的是,许多Vuex的插件并不直接监视状态来跟踪更改,而是依赖于突变。因此,他们看不到状态的“越界”更改。

  

那么mutationsactions到底有什么区别?

动作(如突变)也驻留在商店的模块中,并且可以接收state对象。这意味着他们可以也可以直接对其进行突变。那么,两者都具有什么意义呢?如果我们认为必须使变异保持小而简单,则意味着我们需要一种替代方法来容纳更详尽的业务逻辑。行动是做到这一点的手段。并且由于我们已经建立了较早的vue-devtools,并且插件知道通过Mutations进行的更改,因此我们应继续在操作中使用它们。它确保了一定程度的一致性。

经常强调动作可以是异步的,而突变通常不是异步的。尽管您可以将这种区别表示为应将突变用于同步的任何事物(对于异步的事物则应使用动作),但是例如,如果您需要提交多个突变,则会遇到困难,或者您需要检查由吸气剂计算出的值,因为突变不会接收任何对象。

这引起了一个有趣的问题。

  

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

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

因此,防止突变访问getter意味着后者提供的更复杂的状态检查需要在某个地方重复。可以在调用突变之前完成它(难闻的气味),或者调用者必须以某种方式知道该突变需要吸气剂并将其传递给他人(笨拙),或者必须在突变内复制吸气剂的逻辑,而不会带来额外的好处缓存(恶臭)。

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 
        }
    }
}

以上内容对我来说似乎有些令人费解。我认为这表明在确保容纳vue-devtools的同时,可能已经在Vuex设计中做出了许多决定。

答案 4 :(得分:5)

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

时间机器调试使用状态快照,并显示操作和突变的时间线。从理论上讲,我们可能只有actions和状态设定者和吸气剂同时描述突变的记录。但那时:

  • 我们会有不纯的输入(异步结果),这会导致setter和getter。这在逻辑上很难遵循,不同的异步设置器和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无法知道何时实际调用回调 - 任何状态变异在回调中执行的操作基本上是不可跟踪的!

答案 5 :(得分: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.
    }
  }
})
  

动作处理程序(增量)接收一个公开同一组的上下文对象   商店实例上的方法/属性,因此您可以调用   context.commit提交变异,或访问状态和getter   通过context.state和context.getters

答案 6 :(得分:5)

我已经专业使用Vuex大约3年了,这就是我想出的事情:动作和突变之间的本质区别,如何从它们一起很好地使用中受益以及如何生活如果使用不当会更难。

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

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

基本思想是:

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

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

  • 我们有一个向用户提供订单列表的组件,可以删除这些订单
  • 这些组件已映射了一个商店getter(deletableOrders),它是具有ID的对象的数组
  • 该组件在订单的每一行上都有一个按钮,其单击映射到商店操作(deleteOrder),该操作将订单对象传递给它(我们将记住,它来自商店列表本身)
  • 商店deleteOrder操作执行以下操作:
    • 它验证删除
    • 它存储要临时删除的订单
    • 它按顺序提交ORDER_DELETED突变
    • 它发送API调用以实际删除订单(是的,在修改状态之后!)
    • 它等待调用结束(状态已更新),并且在失败时,我们使用先前保留的顺序调用ORDER_DELETE_FAILED变异。
  • ORDER_DELETED突变将简单地从可删除订单列表中删除给定订单(这将更新吸气剂)
  • 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
  }
}

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

答案 7 :(得分:4)

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

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

答案 8 :(得分:1)

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

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;

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

答案 9 :(得分:1)

突变:

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

答案 10 :(得分:0)

1.从docs

  

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

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

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

2.我们调用变异,我们可以直接改变状态。我们也可以通过以下方式改变状态:

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

Actions旨在处理更多其他事情,我们可以在那里做很多事情(我们可以使用异步操作)然后通过调度变异在那里改变状态。

答案 11 :(得分:0)

因为没有没有突变的州!提交时 - 执行一个以可预见的方式改变状态的逻辑。变异是设置或更改状态的唯一方法(因此没有直接的变化!),而且 - 它们必须是同步的。该解决方案驱动了一个非常重要的功能:突变正在登录devtools。这为您提供了极佳的可读性和可预测性!

还有一件事 - 行动。正如所说 - 行动提交突变。所以他们不会改变商店,也不需要同步。但是,他们可以管理额外的异步逻辑!

答案 12 :(得分: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和React之类的库流行的模块化方式保持一致。