Vuex Action vs Mutations

我是研究僧i 提交于 2019-11-27 10:30:50
Kaicui

Question 1: Why the Vuejs developers decided to do it this way?

Answer:

  1. When your application becomes large, and when there are multiple developers working on this project, you will find the "state manage" (especially the "global state"), will become increasingly more complicated.
  2. The vuex way (just like Redux in react.js) offers a new mechanism to manage state, keep state, and "save and trackable" (that means every action which modifies state can be tracked by debug tool:vue-devtools)

Question 2: What's the difference between "action" and "mutation"?

Let's see the official explanation first:

Mutations:

Vuex mutations are essentially events: each mutation has a name and a handler.

import Vuex from 'vuex'

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

Actions: Actions are just functions that dispatch mutations.

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

Here is my explanation of the above:

  • mutation is the only way to modify state
  • mutation doesn't care about business logic, it just cares about "state"
  • action is business logic
  • action can dispatch more than 1 mutation at a time, it just implements the business logic, it doesn't care about data changing (which manage by mutation)

Mutations are synchronous, whereas actions can be asynchronous.

To put it in another way: you don't need actions if your operations are synchronous, otherwise implement them.

I think the TLDR answer is that Mutations are meant to be synchronous/transactional. So if you need to run an Ajax call, or do any other asynchronous code, you need to do that in an Action, and then commit a mutation after, to set the new state.

I believe that having an understanding of the motivations behind Mutations and Actions allows one to better judge when to use which and how. It also frees the programmer from the burden of uncertainty in situations where the "rules" become fuzzy. After trying to reason about their respective purpose I came to the conclusion that there are definitely wrong ways to use them, but I don't think that there's a canonical approach.

Let's start by understanding why we even go through either Mutations or Actions.

Why go through the boilerplace in the first place? Why not change state directly in components?

Strictly speaking you could change the state directly from your components. The state is just a JavaScript object and there's nothing magical that will revert changes that you make to it.

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

However, by doing this you're scattering your state mutations all over the place. You lose the ability to simply just open a single module housing the state and at a glance see what kind of operations can be applied to it. Having centralized mutations solves this, albeit at the cost of some boilerplate.

// 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)
}
...

I think if you replace something short with boilerplate you'll want the boilerplate to also be small. I therefore assume that mutations are meant to be very thin wrappers around native operations on the state, with almost no business logic. In other words, mutations are meant to be mostly used like setters.

Now that you've centralized your mutations you have a better overview of your state changes and since your tooling (vue-devtools) is also aware of that location it makes debugging easier. It's also worth keeping in mind that many Vuex's plugins don't watch the state directly to track changes, they rather rely on mutations for that. "Out of bound" changes to the state are thus invisible to them.

So mutations, actions what's the difference anyway?

Actions, like mutations, also reside in the store's module and can receive the state object. Which implies that they could also mutate it directly. So what's the point of having both? If we reason that mutations have to be kept small and simple, it implies that we need an alternative means to house more elaborate business logic. Actions are the means to do this. And since as we have established earlier vue-devtools and plugins are aware of changes through Mutations, we should keep using them from our actions. It ensures a level of consistency.

It's often emphasized that actions can be asynchronous, whereas mutations are typically not. Although you could see that distinction as an indication that mutations should be used for anything synchronous (and actions for anything asynchronous), you'd run into difficulties if for instance you needed to commit more than one mutations, or if you needed to check a value computed by a getter, as mutations receive neither object.

Which leads to an interesting question.

Why don't mutations receive getters?

I haven't found a satisfactory answer to this question, yet. I have seen some explanation by the core team which I found moot at best. If I summarize their usage, getters are meant to be computed (and often cached) extensions to the state. In other words, they're basically still the state, albeit that requires some upfront computation cost and in read-only mode. That's at least how they're encouraged to be used.

Therefore preventing mutations from accessing getters means that the more involved state checks provided by the latter need to be duplicated somewhere. It can be done prior to calling a mutation (bad smell), or the caller must know somehow that the mutation needs the getter and passes it down (funky), or the getter's logic must be duplicated inside the mutation, without the added benefit of caching (stench).

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

The above seems a bit convoluted to me. I took it as an indication that a number of decisions in the design of Vuex were probably made while ensuring to accommodate vue-devtools.

According to the docs

Actions are similar to mutations, the differences being that:

  • Instead of mutating the state, actions commit mutations.
  • Actions can contain arbitrary asynchronous operations.

Consider the following snippet.

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) receive a context object which exposes the same set of methods/properties on the store instance, so you can call context.commit to commit a mutation, or access the state and getters via context.state and context.getters

Disclaimer - I've only just started using vuejs so this is just me extrapolating the design intent.

Time machine debugging uses snapshots of the state, and shows a timeline of actions and mutations. In theory we could have had just actions alongside a recording of state setters and getters to synchronously describe mutation. But then:

  • We would have impure inputs (async results) which caused the setters and getters. This would be hard to follow logically and different async setters and getters may surprisingly interact. That can still happen with mutations transactions but then we can say the transaction needs to be improved as opposed to it being a race condition in the actions. Anonymous mutations inside an action could more easily resurface these kinds of bugs because async programming is fragile and difficult.
  • The transaction log would be hard to read because there would be no name for the state changes. It would be much more code-like and less English, missing the logical groupings of mutations.
  • It might be trickier and less performant to instrument recording any mutation on a data object, as opposed to now where there are synchronously defined diff points - before and after mutation function call. I'm not sure how big of a problem that is.

Compare the following transaction log with named mutations.

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

With a transaction log that has no named mutations:

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

I hope you can extrapolate from that example the potential added complexity in async and anonymous mutation inside actions.

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

Now imagine we are debugging the app and looking at the devtool's mutation logs. For every mutation logged, the devtool will need to capture a "before" and "after" snapshots of the state. However, the asynchronous callback inside the example mutation above makes that impossible: the callback is not called yet when the mutation is committed, and there's no way for the devtool to know when the callback will actually be called - any state mutation performed in the callback is essentially un-trackable!

The main differences between Actions and Mutations:

  1. Inside actions you can run asynchronous code but not in mutations. So use actions for asynchronous code otherwise use mutations.
  2. Inside actions you can access getters, state, mutations (committing them), actions (dispatching them) in mutations you can access the state. So if you want to access only the state use mutations otherwise use actions.

This confused me too so I made a simple demo.

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;

After researching this, the conclusion I came to is that mutations are a convention focused only on changing data to better separate concerns and improve logging before and after the updated data. Whereas actions are a layer of abstraction that handles the higher level logic and then calls the mutations appropriately

Mutations:

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

Actions:

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

In Redux Way

Mutations are Reducers
Actions are Actions

Why Both ??

When the application growing , coding and lines will be increasing , That time you have to handle the logic in Actions not in the mutations because mutations are the only authority to change the state, it should be clean as possible.

1.From docs:

Actions are similar to mutations, the differences being that:

  • Instead of mutating the state, actions commit mutations.
  • Actions can contain arbitrary asynchronous operations.

The Actions can contain asynchronous operations, but the mutation can not.

2.We invoke the mutation, we can change the state directly. and we also can in the action to change states by like this:

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

the Actions is designed for handle more other things, we can do many things in there(we can use asynchronous operations) then change state by dispatch mutation there.

Because there’s no state without mutations! When commited — a piece of logic, that changes the state in a foreseeable manner, is executed. Mutations are the only way to set or change the state (so there’s no direct changes!), and furthermore — they must be synchronous. This solution drives a very important functionality: mutations are logging into devtools. And that provides you with a great readability and predictability!

One more thing — actions. As it’s been said — actions commit mutations. So they do not change the store, and there’s no need for these to be synchronous. But, they can manage an extra piece of asynchronous logic!

It might seem unnecessary to have an extra layer of actions just to call the mutations, for example:

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

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

So if calling actions calls logout, why not call the mutation itself?

The entire idea of an action is to call multiple mutations from inside one action or make an Ajax request or any kind of asynchronous logic you can imagine.

We might eventually have actions that make multiple network requests and eventually call many different mutations.

So we try to stuff as much complexity from our Vuex.Store() as possible in our actions and this leaves our mutations, state and getters cleaner and straightforward and falls in line with the kind of modularity that makes libraries like Vue and React popular.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!