Published on

Vuex3.x 快速入门

Reading time
8 分钟
作者

自入职以来使用 Vue 差不多两年了,虽然一直知道 Vuex 的存在,也完整浏览过官方文档,奈何原项目不大,一直没有实践的机会。近期加入了新的项目,使用的技术栈是 Vue2.7 和 Vuex3.x,趁着这个机会加深印象并做个记录。

简介

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

在开发过程中,或多或少都会遇到组件之间需要通信的情况:父子组件、兄弟组件,甚至是隔代组件、多个同级组件等等,通常情况下使用 prop 属性和事件就可以解决。

随着组件之间的层级增多,或是多个组件甚至全局都需要共享某些数据时,prop 属性和事件就不太适用了,不仅写起来更加复杂,代码的可读性和可维护性也会大大降低,此时就需要一个状态管理模式——Vuex。这样的需求产生地如此自然,就像 Redux 作者 Dan Abramov 所说:

Flux 架构就像眼镜:您自会知道什么时候需要它。

基础用法

正式开始之前,先看一张官方文档中的图片

Vuex

Vuex 的部分可以看到三个关键词:State、Mutations、Actions,这些要素和视图构成了一个循环,同时这些要素也是 Vuex 的核心概念。

接下来再看一段代码,看不懂没有关系,可以大致看出这是一个 store 对象,对象中存储了上文提到的要素。state 中存在属性,mutations 和 actions 中存储的都是方法

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

State

类似 Vue 中的 data,可以用来存储需要共享的响应式数据,初始化方式已经在上文提过

const store = new Vuex.Store({
  state: {
    count: 0
  }
})

那么数据如何取出来使用呢,这时候需要使用计算属性 (computed)

// 创建一个 Counter 组件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
	  // 从store中的state取出count属性
    count () {
      return store.state.count
    }
  }
}

还是相当直观的,就像获取普通对象中的属性一样。每当store.state.count变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。

这种写法有一个缺点,每个子组件都需要import store,这时可以把 store 挂载为全局属性

const app = new Vue({
  el: '#app',
  // store: store的简写
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

此时每个子组件都可以使用this.$store的方式获取 store 中的内容

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

此外,Vuex 还提供了辅助函数来简化获取数据的过程,但是为了降低心智负担,此处先按下不表。

Mutation

光有初始化的数据和读取数据的方法并没有什么用,还需要有方法更新数据,这时候就要用到mutation,依然以计数器为例

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

mutation 类似事件,上述代码注册了一个类型为increment的事件,调用时不可直接使用类似mutations.increment()的方法,而是需要用相应的类型调用store.commit方法

store.commit('increment')

此外,commit 时可以传递参数,参数最好使用对象的形式,这样不仅可以增加可读性,传递多个参数也更方便

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

看到这里,不知道你是否会有一个疑问:直接调用 increment 方法修改 state 中的数据不香吗,为什么要用 mutation,这岂不是多此一举,能不能去掉这一步?

理论上来说,是可以去掉的,在另一个 Vue 状态管理库 Pinia 中提到

Pinia API 与 Vuex(4及以下版本) 也有很多不同,即:

  • mutation 已被弃用。它们经常被认为是极其冗余的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。

可以看出 mutation 其实是为了 devtools 引入的,具体原因大概是因为异步同步操作的记录,感兴趣的话可以看一下这两篇文章

需要注意的是,mutation 必须是同步函数,那么异步操作如何解决,接下来让我们看一下 action

Action

action 类似于 mutation,不同在于:

  • action 提交的是 mutation,而不是直接变更状态
  • action 可以包含任意异步操作

举一个简单的例子

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

写法上与 action 类似,但接受的参数是与 store 实例具有相同方法和属性的 context 对象,可以通过调用context.commit提交一个 mutation,或者通过context.state来获取 state。需要注意的是,context 并不是 store 实例本身。

此外,action 还可以执行异步操作

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

还记得 mutation 的使用方式是 store.commit 吗,action 的使用方式是 store.dispatch。和 mutation 类似,action 也可以附加参数。

store.dispatch('incrementAsync', {
  amount: 10
})

至此,Vuex 最基础的概念及用法已经介绍完了,但是为了更简洁优雅的代码和文件结构,还有三个概念需要引入

进阶用法

Getter

Vuex 允许我们在 store 中定义 getter,类似于 Vue 中的 computed 属性,getter 的返回值会根据它的依赖被缓存起来,且只有依赖值发生了改变才会被重新计算。

看一个简单易懂的例子

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

doneTodos 根据 state 中的 todos 数组过滤了已完成的条目,它的使用方式与 state 中的属性相同

辅助函数

State 章节中提到,使用形如store.state.count的方式获取 state 中的属性需要在每个子组件中引入 store,虽然将 store 挂载为全局属性可以解决这一问题,但使用时依然无法避免一遍一遍地调用this.$store,且每需要使用一个属性就要调用一次。

因此 Vuex 提供了一系列辅助函数,它们主要用以简化代码

以 state 为例,我们可以使用 mapState 方法将 state 中的属性映射至当前组件的 computed 中,这样不仅可以一次引入多个属性,使用时也更加方便

computed: {
  // 当前组件其他计算属性
  localComputed () { /* ... */ },

  // 使用对象展开运算符将此对象混入到 computed 中
  ...mapState({
    // 映射 this.name 为 store.state.name
    'name',
    // 映射 this.age 为 store.state.age
    'age'
  })
}

methods: {
  getUserInfo () {
    return `My name is ${this.name}, ${this.age} years old.`;
  }
}

同理,getter、mutation、action 也有各自的辅助函数,只是使用的位置稍有区别,详细的使用方法可以在此处查询:组件绑定的辅助函数

也许有时候会看到这样的代码

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']()
  ])
}

或者这样的代码

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

虽然看起来和之前介绍的用法有一些不同,但实际上只是多了一个可选参数:命名空间,该参数是由 module 引入的。

Module

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

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块——从上至下进行同样方式的分割

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 的状态

通过添加namespaced: true的方式可以使 module 成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名,该方法也适用于层层嵌套的子模块

const module = {
  namespaced: true,
  state: () => ({ ... }),
  mutations: {
    login () { ... } // -> commit('a/login')
  },
  actions: {
    login () { ... } // -> dispatch('a/login')
  },
  getters: {
    isAdmin () { ... } // -> getters['a/isAdmin']
  },
  modules: {
    b: {
      namespaced: true,
      state: () => ({ ... }),
      getters: {
        isVip () { ... } // -> getters['a/b/isVip']
      }
      // ...
    }
  }
}

const store = new Vuex.Store({
  modules: {
    a: module
  }
})