Vuex学习笔记


前言

写在开篇

看到身边的大佬一个二个的秀到飞起,我也要加把劲才是

更新日志

2021/11/01
写下这篇博文,因为难以解决的bug被劝退到自闭
2022/02/13
遇到了一个好的机会,所以又重新拾起这部分内容,更棒的是有仙人指路一下子豁然开朗起来

特别鸣谢

感谢19级软件工程朱珂江学长凌晨的技术指导,我这才得以顺利入门

概览

什么是Vuex

Vuex是一个专为Vue.js应用开发的状态管理模式 和 库(与Vue 3.x对应的是Vuex 4),

采用集中式存储管理应用的所有组件的状态,并且已响应的规则来保证状态以一种可以预测的方式发生变化。

Vuex集成到Vue的官方调试工具devtools extension,提供了诸如零件配置的time-travel调试、状态快照导入导出等高级调试功能

单向数据流的弊端

由于vue提倡的是单向数据流,如果组件与组件层层嵌套,那么利用父子间通信的方式进行数据传递就相当麻烦(其实也可以通过 发布和订阅 来解决这种问题)

在制定出解决这个问题的方案之前,我们先了解一下单向数据流中的几个部分:

状态
驱动应用的数据源
视图
以声明的方式将状态映射到视图
操作
响应在视图上的用户输入导致的状态变化

它们之间的关系像是这样
image.png

Vuex状态管理模式

vuex就相当于给所有需要数据通信的组件创建一个公共的父级,这样进行数据传递的话就快多了

(但是使用Vuex的成本比较高,所以只有中大型项目才使用)

“store”基本上就是一个容器,它包含着你的应用中大部分的**状态 (state)**。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

Vuex中store原理示意图

什么时候使用Vuex

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

上面的是官方文档中的介绍,下面的是某大佬的个人理解:

管你小中大型应用,我就想用就用呗,一个长期构建的小型应用项目,谁能知道项目需求以后会是什么样子,毕竟在这浮躁的时代,需求就跟川剧变脸一样快,对不对?毕竟学习了 Vuex 不立马用到项目实战中,你永远不可能揭开 Vuex 的面纱。项目中使用多了,自然而然就会知道什么时候该用上状态管理,什么时候不需要。老话说的好熟能生巧,你认为呢?
(括弧 – 先了解好Vuex 一些基本概念,然后在自己的项目中使用过后,再用到你公司项目上,你别这么虎一上来就给用上去了~)

简单使用

安装版本问题

如果你仔细阅读了本博客的更新日志,你会发现第一次更新和第二次更新之间相差了近3个月,而这是因为一个当时未得到解决的bug劝退了我,所以停止了学习的步伐

但是最近由于一个项目特别适合用vuex,我便觉得这该是一个非常好的机会啊,就重新拾起了学习vuex的热情

使用如下npm命令安装:

npm install vuex --save

但是在我代码没有问题的前提下,这个bug依旧出现:

Uncaught TypeError: Object(...) is not a function

我也因此在这里又卡顿了两三天,后来经过朱老师的点拨,才终于豁然开朗了
太卷了

原来这是因为Vue和VueX的版本的关系,我们这里用的是Vue2和VueX4.x,版本不匹配所以导致了报错
所以此时正确的操作应该是:

# 直接npm install vuex而不指定版本的话就是安装最新版(4.x),
# 版本指定第一位即可
npm install vue@3 --save

正确的版本对应关系如下:

Vue版本 VueX版本
Vue2.x VueX3.x
Vue3.x VueX4.x
Vue3.x(猜测,暂未查证) Pinia(可以看做VueX5.x)

这里个人习惯使用Vue2.x,所以下文如未特殊说明,皆为Vue2.x

基本文件结构

|—— index.html
|—— main.js
|—— api
| |____ …
|—— components
| |____ App.vue
| |____ …
|—— store # 核心文件夹
|____ index.js # 组装导出模块
|____ action.js # 根级action
|____ mutations.js # 根级mutation
|____ modules # 各种模块
|____ ….

上述文件结构是针对于大型应用的标准文件结构,这里我们只是入门,所以并不严格遵循,而是将所有js文件糅合成一个store.js,并且暂时先不做模块化

image.png

然后在store.js和main.js中分别写入:

store.js和main.js代码示例

Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到

基本数据使用

在进行了上述操作之后,如果想要使用数据msg我们只需要使用如下语句:

this.$store.state.msg 
// 这里的this在template部分中可以省略
// 但是在JS部分中是不可以省略的

使用数据并不困难,我们需要注意的是如何修改数据,我们虽然可以通过对上述语句赋值的形式

来修改,但是我们并不推荐这样做,因为这些数据应该被视为private,但是我们又希望所有组件都可以访问并修改它们,这时候就该想到getter和setter

getter我们会在后面提到

这里的setter被放到mutations里面,而要访问到这些setter则需要用this.$store.commit,例如:

基本数据修改

不建议直接使用this.$store.state直接修改,一是因为这样修改后没有任何提示,若出错则可能难以追溯问题来源;二是因为这样修改则形成了双向绑定结构,对于数据不安全;

所以推荐像下面这样:

image.png

这里我们可以发现,mutation中的函数的第一个参数是state,这个是固定的,而如果需要更多的参数则在后面依次添加,传参只需要像这样:

this.$store.commit('change', '我是参数')

注意这里如果要追加参数,不能用逗号隔开来继续添加,而是将参数打包成一个对象传入——当然,函数声明也要遵循这一点

另外,还要注意,mutation只能是同步任务
如何异步后面会提到

基本数据增删

并不需要使用额外的语法,只需要我们像下面这样就能完成

由于新添加的属性如果要规范使用的话还得添加额外的mapState语句,所以我们会再包装一层:

this.$store.state.obj = { ...this.$store.obj, new: 233 }

或者这样写也行:

Vue.set( state.obj, 'new', 123 )

mutation的两种添加方式

归根结底commit是触发mutation中函数的方式之一,我们还能通过mapMutations来触发,不过这里暂时不讨论这个

核心概念

在学会基础使用之后,再来稍微深入一点,接下来这部分内容省略了已经提及过的部分

State

单一状态树

用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT (opens new window))”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段

组件中获取 Vuex 状态

那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性 (opens new window)中返回某个状态:

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

每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM
相较于直接使用store.state.count访问,更推荐使用

mapState 辅助函数

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

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

import { mapState } from 'vuex'

export default {
  // ...
   computed: { 
    ...mapState(['msg'])
  }
}

在这样操作之后,你就能直接使用msg来访问变量,而不是$store.state.msg
mapState的原理就是字符串映射,将msg映射成为一个与之相关的函数,并放入一个对象返回

请看下面的例子:

Mutations

同步原则

mutation必须是同步函数,
举个例子:

devtool在debug过程中,需要记录mutation的前一状态和后一状态,
但是在mutation执行的时候,异步操作还未执行,这就使得状态无法跟踪,
但是这种操作在视图层渲染上却是没有问题的——可正是这样,又导致了数据发生了不同步

那么异步的操作要到哪里执行呢?我们后面再提

mapMutations辅助函数

和mapState类似,mupMutation作为触发mutation内的函数的第二种方式

有两种使用方式,具体语法如下:

mapMutations的两种使用方式

Actions

处理异步

Action是专门用于处理任务的

如果通过异步操作更改数据,必须通过action,而不能使用mutation,但是在action中还是要通过触发mutation的方式间接变更数据——只有mutation才能修改state

也就是:

image.png

其中的context可以理解为new Vuex.Store实例对象

这里要注意,actions中的函数不是用commit触发了,而是用dispatch

this.$store.dispatch('plusAsync')

如果需要传参,那么也是和commit一个道理

如果是同步的内容,那么可以用dispatch或commit,异步只能dispatch

Vuex模式示意

mapActions辅助函数

dispatch是第一种方式,而第二种方式是mapActions

方法跟之前的辅助函数如出一辙,此处不再赘述

Getters

Getter是用于对Store中的数据进行加工处理,然后形成新的数据(深拷贝),并不修改原本的数据

可以认为是 store 的计算属性,就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算

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

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想

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

通过属性访问

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

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

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

通过方法访问

你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

(这个方法真的是震撼我一年)

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

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

mapGetters辅助函数

依然是那么回事,只不过说mapGetters一般只能放到computed中

import { mapGetters } from 'vuex'

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

Modules

由于是Vuex是单一状态树,所以当状态过多的时候,store对象就会相当臃肿,为了解决这种问题,就提出了将store分割成多个模块的思想

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

基本使用就是向上面这样,更多深入的地方以后再进行学习

参考资料

Junting的博文:https://www.jianshu.com/p/c6356958ca52
Vuex官方文档:https://vuex.vuejs.org/zh/guide/mutations.htm
Vuex从入门到实战:https://www.bilibili.com/video/BV1h7411N7bg


文章作者: Serio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Serio !
  目录