跳至主要內容

Vuex使用总结

CharHua大约 12 分钟VueVuex4状态管理

一、 Vuex 4介绍

1、Vuex状态管理模式

1、通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

2、Vuex 使用单一状态树: 用一个对象就包含了全部的应用层级的状态; 采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源; 这也意味着,每个应用将仅仅包含一个 store 实例; 单状态树和模块化并不冲突,后面我们会讲到module的概念;

3、单一状态树的优势: 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难; 所以Vuex也使用了单一状态树来管理应用层级的全部状态; 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便 的管理和维护;

2、Vuex原理

vuex
vuex

二、安装

1、引用

在 Vue 之后引入 vuex 会进行自动安装:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

2、npm

npm install vuex@next --save

三、基本使用

1、新建store文件夹,在里面新建index.js文件,用于Vuex的main出入口。

index.js下:

import { createStore } from 'vuex';
// 创建一个新的 store 实例
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

在项目main.js中:

import { createApp } from 'vue';
const app = createApp({ /* 根组件 */ })

// 将 store 实例作为插件安装
app.use(store)

之后可在组件option API中:

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

注意:

1、修改state中数据时,最好通过提交mutation方式,而非直接改变 $store.state.xxx。因为我们想要更明确地追踪到状态的变化,即通过devtools。

2、由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。

四、核心概念

1、State

Vuex 的状态存储是响应式的。

1、在options API中使用

 computed: {
    count () {
      return this.$store.state.count
    }

2、辅助函数mapState

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:

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

export default {
  computed: mapState({
    // 1、使用箭头函数方式
    count: state => state.count,

    // 2、使用字符串参数方式,其等同于 `state => state.count`
    countAlias: 'count',

    // 3、使用常规函数,这样能使用 `this` 获取局部状态,
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

当然有更简便写法。

当映射的计算属性的名称与 state 的子节点名称相同时,我们可以给 mapState 传一个字符串数组。

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

3、对象展开写法

当我们在computed中将局部计算属性与mapState混合使用时,我们可以使用对象展开运算符。

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

注意:

使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。

2、Getter

getter类似于store中的计算属性,可以简化或者计算一些数据(状态)。

1、在options API中使用

**1、参数:**getter接收两个参数:state,getters(可选),其中getters为其他getter

**2、返回值:**Getter 会暴露为 store.getters 对象。当然也可以让 getter 返回一个函数,来实现给 getter 传参。

3、定义:

//返回对象
getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    }
  }

//返回函数
getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}

4、访问:

//返回值为对象时。
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

//返回值为函数时,执行它。
this.$store.getters.getTodoById(2)

2、辅助函数mapGetters

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'

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

当然传入时也可以给它取另一个名字:

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

3、Mutation

1、更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)**和一个**回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方。

2、 mutation 必须是同步函数,而不能使用异步调用,若需要异步请求可在action中使用。

1、在options API中使用

1、参数

接受第一个参数:state,第二个参数:payload(载荷,最好为一个对象,以承载更多字段)。

2、定义

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

3、在store中使用

//一般在actions中,组件调用actions后,在action中执行mutation去修改state。因为action不能直接访问state。
store.commit('increment', {
  amount: 10
})
//用对象风格提交
store.commit({
  type: 'increment',
  amount: 10
})

4、在组件中提交Mutation

可以在组件中使用 this.$store.commit('xxx') 提交 mutation。

5、使用mapMutations辅助函数

在methods中使用mapMutations

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')`
    })
  }
}

2、使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然。

先在单独文件定义常量:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'

在vuex中使用:

// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

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

使用常量在多人协作中比较有用,个人项目的话可以不使用。

4、Action

Action类似于mutation,不同在于:

1、Action提交的是mutation,而不是直接变更状态;

2、Action可以包含任意异步操作;

1、在options API中使用

1、参数

1、第一个参数:context ,它是一个和store实例均有相同方法和属性的context对象,我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来 获取 state 和 getters。

2、第二个参数:payload(载荷),Actions 支持同样的载荷方式和对象方式进行分发。

2、在store中定义

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
//当然context也可以用解构
actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
//传入其他参数
actions:{
    increment ({commit},info){
        //...
    }
}

2、分发

Action 通过 store.dispatch 方法触发。

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

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

4、在组件中使用(分发)

1、在组件中使用 this.$store.dispatch('xxx') 分发 action。

2、也使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)。

使用mapActions进行分发时,有两种写法:

1、对象写法:

import { mapActions } from 'vuex'
export default {
  methods: {
    ...mapActions({
      add: 'increment'
    })
  }
}
 // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`

2、数组写法:

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)`

5、Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可 能变得相当臃肿;

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);

每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块;

1、基本使用

1、在store目录下,新建modules目录,在此目录下新建模块仓库。

如 home.js:

const homeModule = {
  namespaced: true,
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
export default homeModule

1、对于模块内部的 mutation ,接收的第一个参数是模块的局部状态对象,第二个参数是payload(载荷)。

2、对于模块内部的 getter,接收的第一个参数是模块的局部状态对象,第二个参数是getters。第三个参数是根节点状态: rootState。

3、要启用命名空间:

namespaced: true
const userModule = {
  namespaced: true,
  state: () => ({ ... }),
                 
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  },
      
  actions: { ... },
}
export default homeModule

2、统一导入。

store/index.js中:

import { createStore } from "vuex";
import home from './modules/home';
import user from './modules/user';

const store = createStore({
   //根仓库
  state() {
    return {
      rootCounter: 100
    }
  },
  getters: {
    doubleRootCounter(state) {
      return state.rootCounter * 2
    }
  },
  mutations: {
    increment(state) {
      state.rootCounter++
    }
  },
    //注册模块仓库
  modules: {
    home,
    user
  }
});

export default store;

3、在options API中使用

this.$store.state.home.xxx
export default {
  methods: {
    homeIncrement() {
      this.$store.commit("home/increment")
    },
    homeIncrementAction() {
      this.$store.dispatch("home/incrementAction")
    }
  }
}

以上基本使用需要启用命名空间,因为默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。

如果希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

4、命名空间嵌套

const store = createStore({
  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']
          }
        }
      }

2、命名空间中具体使用

启用了命名空间namespaced后,其他进阶使用。

1、局部模块访问全局内容

局部模块希望使用全局 state 和 getter,rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

//foo.js
const 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) { ... }
    }
  }
export default foo;

2、在局部模块注册成全局action

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

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

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

3、在组件中使用辅助函数

1、写法一
computed: {
  ...mapState({
    homeCounter: state => state.home.homeCounter
  }),
  ...mapGetters({
    doubleHomeCounter: "home/doubleHomeCounter"
  })
},
methods: {
  ...mapMutations({
     increment: "home/increment"
  }),
  ...mapActions({
     incrementAction: "home/incrementAction"
  }),
}
2、写法二
computed: {
  ...mapState('home', {
    homeCounter: state => state.homeCounter,
  })
  ...mapState('home', ["homeCounter"])
},
methods: {
  ...mapActions('home', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}
3、写法三(推荐)

通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('home')

export default {
  computed: {
    // 在 `home` 中查找
    ...mapState(["homeCounter"])
  },
  methods: {
    // 在 `home` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

五、组合式 API中使用

1、基本使用

通过调用 useStore 函数,来在 setup 钩子函数中访问 store。这与在组件中使用选项式 API 访问 this.$store 是等效的。

1、使用State、Getter

import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
  setup () {
    const store = useStore();
    // 在 computed 函数中访问 state
	const count = computed(() => store.state.count);
      
    return {
      count,
      // 在 computed 函数中访问 getter
      double: computed(() => store.getters.double)
    }
  }
}

2、使用Mutation、Action

import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore();
    // 使用 mutation      
	const increment: () => store.commit('increment');
      
    return {
      increment,
      // 使用 action
      asyncIncrement: () => store.dispatch('asyncIncrement')
    }
  }
}

2、封装辅助函数使用

对于大项目来说,在composition API中基本使用时可能重复代码过于多以及不够简便,于是可以配合使用mapXXX辅助函数来解决。注意,辅助函数并不能像在options API中直接使用,因此需要单独封装。

1、简单封装使用

1、State

第一步:封装hook

在某hooks目录中新建useState.js:

import { computed } from 'vue'
import { mapState, useStore } from 'vuex'

export function useState(mapper) {
  // 拿到store独享
  const store = useStore()

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapState(mapper)

  // 对数据进行转换
  const storeState = {}
  Object.keys(storeStateFns).forEach(fnKey => {
    const fn = storeStateFns[fnKey].bind({$store: store})
    storeState[fnKey] = computed(fn)
  })

  return storeState
}

第二步:引入使用

import { useState } from '../hooks/useState'

export default {
  setup() {
    const storeState = useState(["counter", "name", "age", "height"])
    const storeState2 = useState({
      sCounter: state => state.counter,
      sName: state => state.name
    })

    return {
      ...storeState,
      ...storeState2
    }
  }
}

2、高级封装