基本概念
vue进行开发过程中有没有遇到这样一种场景,就是有些时候一些数据是一种通用的共享数据(比如登录信息),那么这类数据在各个组件模块中可能都会用到,如果每个组件中都去后台重新获取那么势必会造成性能浪费,为了解决这一问题一个新的状态管理工具 - vuex
就应运而生
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
什么是“状态管理模式”?
这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
安装方式
CDN引用
https://unpkg.com/vuex@4
Unpkg.com 提供了基于 npm 的 CDN 链接。以上的链接会一直指向npm 上发布的最新版本。
您也可以通过 https://unpkg.com/vuex@4.0.0/dist/vuex.global.js 这样的方式指定特定的版本。
Npm
npm install vuex@next --save
Yarn
yarn add vuex@next --save
核心概念
State
- 单一状态树
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 - 在 Vue 组件中获得 Vuex 状态
由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:
const Counter = {template: `<div>{{ count }}</div>`,computed: {count () {return store.state.count}}}
每当store.state.count变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM
Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过this.$store访问到。让我们更新下Counter的实现:
const Counter = {template: `<div>{{ count }}</div>`,computed: {count () {return this.$store.state.count}}}
- mapState 辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。
为了解决这个问题,我们可以使用 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}})}
- 对象展开运算符
mapState函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给computed属性。但是自从有了对象展开运算符【…】,我们可以极大地简化写法:
computed: {localComputed () { /* ... */ },// 使用对象展开运算符将此对象混入到外部对象中...mapState({// ...})}
对象展开运算符
var obj = {a:1,b:2}
var obj1 = {c:3,d:4}
var obj2 ={…obj,…obj1}
var obj3 = {…obj,a:8,w:66}
var obj4 ={…obj,a:8,w:66,…obj1}
console.log(obj); //{a: 1, b: 2}
console.log(obj1); //{c: 3, d: 4}
console.log(obj2); //{a: 1, b: 2,c: 3, d: 4}
console.log(obj3); //{a: 8, b: 2, w: 66}
console.log(obj4); //{a: 8, b: 2, w: 66, c: 3, d: 4}
Getter
我们需要从 store 中的 state 中派生出一些状态,如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
从 Vue 3.0 开始,getter 的结果不再像计算属性一样会被缓存起来
Getter 接受 state 作为其第一个参数:
const store = createStore({state: {todos: [{ id: 1, text: '...', done: true },{ id: 2, text: '...', done: false }]},getters: {doneTodos (state) {return state.todos.filter(todo => todo.done)}}})
- 通过属性访问
Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:
store.getters.doneTodos
Getter 也可以接受其他 getter 作为第二个参数
getters: {// ...doneTodosCount (state, getters) {return getters.doneTodos.length}}
- 通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {getTodoById: (state) => (id) => {return state.todos.find(todo => todo.id === id)}}
store.getters.getTodoById(2)
getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
- mapGetters 辅助函数
mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性
import { mapGetters } from 'vuex'
export default {// ...computed: {// 使用对象展开运算符将 getter 混入 computed 对象中...mapGetters(['doneTodosCount','anotherGetter',// ...])}}
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = createStore({state: {count: 1},mutations: {increment (state) {// 变更状态state.count++}}})
- 提交载荷(Payload)
你可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload):
mutations: {increment (state, n) {state.count += n}}
store.commit('increment', 10)
- 对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
store.commit({type: 'increment',amount: 10})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:
mutations: {increment (state, payload) {state.count += payload.amount}}
- Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
- 分发 Action
Action 通过 store.dispatch 方法触发:
store.dispatch('increment')
actions: {incrementAsync ({ commit }) {setTimeout(() => {commit('increment')}, 1000)}}
- 在组件中分发 Action
你在组件中使用this.$store.dispatch(‘xxx’)分发 action,或者使用mapActions辅助函数将组件的methods映射为store.dispatch调用(需要先在根节点注入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')`})}}
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿
为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
const moduleA = {state: () => ({ ... }),mutations: { ... },actions: { ... },getters: { ... }}const moduleB = {state: () => ({ ... }),mutations: { ... },actions: { ... }}const store = createStore({modules: {a: moduleA,b: moduleB}})store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
进阶
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
热重载
使用 webpack 的 Hot Module Replacement API,Vuex 支持在开发过程中热重载 mutation、module、action 和 getter。你也可以在 Browserify 中使用 browserify-hmr 插件。
对于 mutation 和模块,你需要使用 store.hotUpdate() 方法:
// store.js
import { createStore } from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'
const state = { ... }
const store = createStore({state,mutations,modules: {a: moduleA}})
if (module.hot) {// 使 action 和 mutation 成为可热重载模块module.hot.accept(['./mutations', './modules/a'], () => {// 获取更新后的模块// 因为 babel 6 的模块编译格式问题,这里需要加上 `.default`const newMutations = require('./mutations').defaultconst newModuleA = require('./modules/a').default// 加载新模块store.hotUpdate({mutations: newMutations,modules: {a: newModuleA}})})}
动态模块热重载
如果你仅使用模块,你可以使用 require.context 来动态地加载或热重载所有的模块。
// store.js
import { createStore } from 'vuex'
// 加载所有模块。function loadModules() {const context = require.context("./modules", false, /([a-z_]+)\.js$/i)const modules = context.keys().map((key) => ({ key, name: key.match(/([a-z_]+)\.js$/i)[1] })).reduce((modules, { key, name }) => ({...modules,[name]: context(key).default}),{})return { context, modules }}
const { context, modules } = loadModules()
const store = new createStore({modules})
if (module.hot) {// 在任何模块发生改变时进行热重载。module.hot.accept(context.id, () => {const { modules } = loadModules()store.hotUpdate({modules})})}
组合式api
可以通过调用 useStore 函数,来在 setup 钩子函数中访问 store。这与在组件中使用选项式 API 访问 this.$store 是等效的。
import { useStore } from 'vuex'
export default {setup () {const store = useStore()}}
访问 State 和 Getter
为了访问 state 和 getter,需要创建 computed 引用以保留响应性,这与在选项式 API 中创建计算属性等效。
import { computed } from 'vue'import { useStore } from 'vuex'
export default {setup () {const store = useStore()return {// 在 computed 函数中访问 statecount: computed(() => store.state.count),// 在 computed 函数中访问 getterdouble: computed(() => store.getters.double)}}}
访问 Mutation 和 Action
要使用 mutation 和 action 时,只需要在 setup 钩子函数中调用 commit 和 dispatch 函数。
import { useStore } from 'vuex'
export default {setup () {const store = useStore()return {// 使用 mutationincrement: () => store.commit('increment'),// 使用 actionasyncIncrement: () => store.dispatch('asyncIncrement')}}}