Global state management in vue project (Vuex)

What is Vuex?

Vuex is a state management pattern developed specifically for Vue.js applications . It uses centralized storage to manage the status of all components of the application, and uses corresponding rules to ensure that the status changes in a predictable way.

What is the state management pattern?

Let's start with a simple Vue counting application:

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template: `
    <div>{
   
   { count }}</div>
  `,
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})

This state self-management application consists of the following parts:

  • state , the data source that drives the application;
  • view , declaratively  maps state  to views;
  • actions , respond to   state changes caused by user input on the view .

Here is a simple illustration representing the idea of ​​"one-way data flow":

 

However, the simplicity of one-way data flow is easily broken when our application encounters multiple components sharing state :

  • Multiple views depend on the same state. ==》The method of passing parameters will be very cumbersome for multi-layer nested components, and it is powerless for state transfer between sibling components.
  • Actions from different views require changing the same state. ==》We often use parent-child components to directly reference or use events to change and synchronize multiple copies of the state.

 By defining and isolating various concepts in state management and maintaining the independence between views and states by enforcing rules, our code will become more structured and easier to maintain. This is the basic idea behind Vuex.

 When should you use Vuex?

  • Shared data and behaviors need to be split;
  • Complex asynchronous logic requires the integration of multiple modules for state evolution and detailed debugging information;
  • Requires third-party plug-ins;
  • Since it has a globally unique data source, it facilitates cross-platform implementation . Similar to NUXT, SSR uses Vuex for front-end and back-end data synchronization.
  • It is necessary to comprehensively consider multiple component life cycles , sequence, and implement specific logic.

start

The core of every Vuex application is the store. A "store" is basically a container that contains most of the state in your application . There are two differences between Vuex and simple global objects:

  1. Vuex's state storage is reactive. When the Vue component reads the state from the store, if the state in the store changes, the corresponding component will be updated efficiently accordingly.

  2. You cannot directly change the state in the store. The only way to change the state in the store is to explicitly commit a mutation . This allows us to easily track every state change, allowing us to implement some tools to help us better understand our application.

The simplest Store

Create a store:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

Get the state object through  store.state and  store.commit trigger state changes through methods: 

store.commit('increment')

console.log(store.state.count) // -> 1

 In order to access properties in a Vue component  this.$store , you need to provide the Vue instance with the created store. Vuex provides a  store mechanism to "inject" the store as an option from the root component to all sub-components:

new Vue({
  el: '#app',
  store: store,
})

 Submit a change from a component's method:

methods: {
  increment() {
    this.$store.commit('increment')
    console.log(this.$store.state.count)
  }
}

 State (single state tree)

Get Vuex state in Vue component

Since Vuex's state store is reactive, the easiest way to read state from a store instance is to return some state in a computed property:

// 创建一个 Counter 组件
const Counter = {
  template: `<div>{
   
   { count }}</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}

Whenever  store.state.count it changes, the calculated properties will be re-calculated and an update of the associated DOM will be triggered.

However, this pattern results in components relying on global state singletons. In a modular build system, state needs to be imported frequently in every component that needs to use it, and state needs to be mocked when testing components.

Vuex  store provides a mechanism to "inject" state from the root component into each child component through options (needs to be called  Vue.use(Vuex)):

const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

By registering options in the root instance  store , the store instance will be injected into all sub-components under the root component, and the sub-components can be  this.$store accessed. Let's update  Counter the implementation below: 

const Counter = {
  template: `<div>{
   
   { count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

mapState Helper function

When a component needs to obtain multiple states, declaring these states as computed properties will be somewhat repetitive and redundant. To solve this problem, we can use  mapState helper functions to help us generate computed properties, allowing you to press fewer keys:

// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

When the name of the mapped calculated attribute is the same as the name of the child node of the state, we can also pass  mapState a string array.

computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])

object spread operator 

mapState The function returns an object. How can we mix this with local computed properties? Often, we need to use a utility function to merge multiple objects into one so that we can pass the final object to  computed a property. But since the object spread operator is available, we can greatly simplify the writing:

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

Getter

Sometimes we need to derive some state from the state in the store, such as filtering and counting the list:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

If multiple components need to use this property, we either need to duplicate the function, or extract a shared function and import it in multiple places - neither method is ideal.

Vuex allows us to define "getters" in the store (which can be thought of as computed properties of the store). Just like computed properties, the return value of a getter is cached according to its dependencies, and is only recomputed when its dependency values ​​change.

Getter accepts state as its first parameter:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Access via properties

Getters are exposed as  store.getters objects and you can access the values ​​as properties:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

Getters can also accept other getters as second arguments:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1

We can use it easily in any component:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

Note that getters are cached when accessed through properties as part of Vue's reactive system.

Access via method

You can also pass parameters to the getter by letting the getter return a function. Very useful when you query arrays in the store.

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

Note that when the getter is accessed through a method, it will be called every time without caching the result.

mapGetters Helper function

mapGetters The helper function simply maps the getters in the store to local computed properties:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

If you want to rename a getter property by another name, use the object form:

...mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

Mutation

The only way to change the state in the Vuex store is to submit a mutation. Mutations in Vuex are very similar to events: each mutation has a string  event type (type)  and a  callback function (handler) . This callback function is where we actually make the state changes, and it accepts state as the first parameter:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

You cannot call a mutation handler directly. This option is more like an event registration: "When a mutation of type is triggered  , call this function." To wake up a mutation handler, you need to call the store.commit  method increment with the corresponding type  :

store.commit('increment')

Submit payload

You can  store.commit pass in additional parameters,  the payload of the mutation :

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

In most cases, the payload should be an object so that it can contain multiple fields and the recorded mutations will be more readable:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

Object style submission

Another way to submit a mutation is to use  type an object containing properties directly:

store.commit({
  type: 'increment',
  amount: 10
})

When using object-style submission, the entire object is passed as payload to the mutation function, so the handler remains unchanged:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

Mutation needs to comply with Vue's response rules

Since the state in the Vuex store is responsive, when we change the state, the Vue component that monitors the state will automatically update. This also means that mutations in Vuex also need to follow the same precautions as using Vue:

  1. It's best to initialize all required properties in your store ahead of time.

  2. When you need to add new properties on an object, you should

Use constants instead of Mutation event types

Using constants instead of mutation event types is a very common pattern in various Flux implementations. This allows tools like linters to be useful, and placing these constants in separate files allows your code collaborators to see at a glance the mutations contained in the entire app:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

It's up to you whether to use constants or not - this can be helpful in large projects where multiple people are working together. But if you don't like it, you don't have to do it.

Mutation must be a synchronous function

An important principle to remember is that  mutations must be synchronous functions . Why? Please refer to the following example:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

Now imagine that we are debugging an app and observing the mutation log in devtool. Every mutation is recorded, devtools needs to capture snapshots of the previous state and the next state. However, in the above example the callback in the asynchronous function in the mutation makes this impossible: because the callback function has not yet been called when the mutation fires, devtools has no idea when the callback function is actually called - essentially Any state changes made within the callback function are not traceable.

Submit Mutation in component

You can use commit mutations in components  this.$store.commit('xxx') , or use  mapMutations helper functions to map methods in components to  store.commit calls (need to be injected at the root node  store).

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

Next step: Action

Mixing asynchronous calls within mutations can make your program difficult to debug. For example, when you call two mutations that contain asynchronous callbacks to change state, how do you know when to call back and which one to call back first? This is why we distinguish between these two concepts. In Vuex, mutations are all synchronous transactions :

store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。

Action

Action is similar to mutation, except that:

  • Action submits a mutation rather than directly changing the state.
  • Action can contain any asynchronous operation.

Let's register a simple action:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

The Action function accepts a context object with the same methods and properties as the store instance, so you can call  context.commit Submit a mutation, or   obtain state and getters through context.state and  . context.gettersWhen we introduce  Modules later  , you will know why the context object is not the store instance itself.

In practice, we often use ES2015  parameter destructuring (opens new window) to simplify the code (especially when we need to call it  commit many times):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

DistributeAction

Action  store.dispatch is triggered via methods:

store.dispatch('increment')

At first glance, it seems unnecessary. Wouldn't it be more convenient for us to distribute mutations directly? In fact, this is not the case. Remember  the restriction that mutations must be executed synchronously ? Action is not restricted! We can perform asynchronous operations inside the action :

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions supports the same payload method and object method for distribution:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

Let’s look at a more practical shopping cart example that involves calling an asynchronous API  and dispatching multiple mutations :

actions: {
  checkout ({ commit, state }, products) {
    // 把当前购物车的物品备份起来
    const savedCartItems = [...state.cart.added]
    // 发出结账请求,然后乐观地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接受一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

Note that we are performing a series of asynchronous operations and recording the side effects (ie, state changes) of the action by submitting mutations.

Distribute Actions in Components

this.$store.dispatch('xxx') You use dispatch actions  in the component  , or use mapActions auxiliary functions to map the component's methods to  store.dispatch calls (you need to inject them in the root node first  store):

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

CombinationAction

Actions are usually asynchronous, so how do you know when the action ends? More importantly, how can we combine multiple actions to handle more complex asynchronous processes?

First, you need to understand  store.dispatch that you can handle the Promise returned by the handler function of the triggered action and  store.dispatch still return the Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

Now you can:

store.dispatch('actionA').then(() => {
  // ...
})

It is also possible in another action:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

Finally, if we utilize  async / await (opens new window) , we can combine actions as follows:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

One  store.dispatch action function can trigger multiple action functions in different modules. In this case, the returned Promise will not be executed until all triggering functions have completed.

Module

Due to the use of a single state tree, all the state of the application will be concentrated into a relatively large object. When an application becomes very complex, store objects have the potential to become quite bloated.

In order to solve the above problems, Vuex allows us to split the store into modules . Each module has its own state, mutation, action, getter, and even nested submodules - divided in the same way from top to bottom:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

Module's local state

For mutations and getters inside a module, the first parameter received is the module’s local state object .

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

Similarly, for actions inside the module, the local state is  context.state exposed, and the root node state is  context.rootState:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

For getters inside the module, the root node status will be exposed as the third parameter:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

Namespaces

By default, actions, mutations and getters inside a module are registered in the global namespace - this allows multiple modules to respond to the same mutation or action.

If you want your module to have a higher degree of encapsulation and reusability, you can  namespaced: true make it a module with a namespace by adding it. When a module is registered, all its getters, actions, and mutations will automatically be named according to the module's registered path. For example:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

Namespace-enabled getters and actions receive localized  getter, dispatch and  commit. In other words, you do not need to add additional space name prefixes within the same module when using module assets. namespaced There is no need to modify the code within the module after changing  the properties.

Access global content (Global Assets) within a namespace module

If you wish to use global state and getters, rootState and  rootGetters will be passed into the getter as the third and fourth parameters, and  context the action will also be passed through the object's properties.

If you need to distribute actions or submit mutations in the global namespace, pass it  to  or  { root: true } as the third parameter   .dispatchcommit

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

Register a global action in a namespace module

If you need to register a global action in a module with a namespace, you can add it  root: trueand place the definition of this action  handler in a function. For example:

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

Bind function with namespace

When using  mapStatemapGettersmapActions and  mapMutations these functions to bind namespace modules, it can be tedious to write:

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

For this case, you can pass the module's spatial name string as the first argument to the above function, so that all bindings will automatically have that module as context. So the above example can be simplified to:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

Furthermore, you can  createNamespacedHelpers create helper functions based on a certain namespace by using. It returns an object with the new component binding helper function bound to the given namespace value:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

Notes to plugin developers

If you develop a plug-in that provides modules and allows users to add them to the Vuex store, you may need to consider the space name of the module. For this case, you can allow the user to specify the space name via the plugin's parameter object:

// 通过插件的参数对象得到空间名称
// 然后返回 Vuex 插件函数
export function createPlugin (options = {}) {
  return function (store) {
    // 把空间名字添加到插件模块的类型(type)中去
    const namespace = options.namespace || ''
    store.dispatch(namespace + 'pluginAction')
  }
}

Module dynamic registration

After the store is created , you can  store.registerModule register the module using the method:

import Vuex from 'vuex'

const store = new Vuex.Store({ /* 选项 */ })

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

You can then   access the module's status via store.state.myModule and  .store.state.nested.myModule

The module dynamic registration function allows other Vue plug-ins to use Vuex to manage state by attaching new modules to the store. For example, the vuex-router-sync (opens new window) plug-in combines vue-router and vuex through a dynamic registration module to implement application routing status management.

You can also use  store.unregisterModule(moduleName) to dynamically unload modules. Note that you cannot use this method to unload static modules (that is, modules declared when creating the store).

Note that you can use  store.hasModule(moduleName) the method to check whether the module has been registered to the store.

retain state

When registering a new module, you will most likely want to preserve past state, for example from a server-rendered application. You can  preserveState archive it via option: store.registerModule('a', module, { preserveState: true }).

When you set it up  preserveState: true , the module is registered and actions, mutations, and getters are added to the store, but state is not. This assumes that the store's state already contains the module's state and you don't want to overwrite it.

module reuse

Sometimes we may need to create multiple instances of a module, for example:

If we use a pure object to declare the state of the module, then the state object will be shared through references, leading to the problem of data contamination between stores or modules when the state object is modified.

data In fact this is the same problem as in Vue components  . So the solution is the same - use a function to declare the module state (only supported in 2.3.0+):

const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutation, action 和 getter 等等...
}

Guess you like

Origin blog.csdn.net/w418856/article/details/132799333