Vue基础知识巩固之全面了解Vuex,比官方更易懂(上)

536 阅读5分钟

前言

写项目很久了,偶尔用到Vuex也是用一些很浅显的功能,就是简单的存储一下用户信息,用的时候取一下,很少深入的使用,现在静下心来想给自己写个项目,在写的过程中,顺便把以往忽略的基础知识学习巩固一下,这篇文章就是记录一下学习Vuex的知识,既然是巩固知识,那那些基础的就直接一笔带过了。

本文面向对象也主要是初学者,所以用词更偏向于易懂,所以建议大家理解之后还是去看一下官方文档,熟悉一下专业术语,并且本文主要挑重点的讲,大家也可以去看一下官方文档查漏补缺。

介绍

什么是Vuex?

在日常的项目开发中,我们经常会遇到一些需要全局存储的变量,需要多个地方使用,比如用户信息,购物车等,在之前,我们采取的方案可能就是设置公共组件或者利用 cookielocalstorage 等本地存储方式进行存储,但这样的方法无疑会带来很多弊端,例如:

  • 需要在多个模块频繁引用
  • 存储格式限制,取值时候需要格式转换
  • 存储结构不够清晰
  • 不是响应式的
  • 无法形成统一规范,接受别人代码需要一定时间理解
  • 无法追踪值的修改记录
  • ……

以上这些问题Vuex都可以统统为我们解决掉,下面我们就来看看Vuex官方是如何解释vuex的

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

以上是vuex官方文档对于vuex的解释,但对于初级vue开发者来说,这样的解释无疑太过官方,过于拗口也难以理解,其实用白话文解释一下,大致就是Vuex是一个仅能在Vue.js环境下使用的一个公共状态存储仓库,这个仓库里可以存储你所经常使用的全局变量,并且为你指定了规定的方式去修改仓库里的变量,并且如果你用这种方式修改仓库里的变量,那么所有的修改记录都可以被保存。

安装及使用

像Vue一样,我们也可以通过script标签引入的方式和模块安装的方式使用Vuex,通常我们会通过Vue-cli新建Vue项目,如果你选择了的话,Vue-cli会自动为我们的新项目安装并引入Vuex,此处不在赘述,后续的内容讲解以在此情况下展开。

Vuex核心概念

简述

Vuex的核心概念如下所示,包括state,Getters,mutations和Actions,下面我们就来分别深入了解他们的作用。

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  getters
})

state

state 就是我们在 Vuex 中存储数据的地方,state 中的数据和 Vue 实例中的 data 一样,也必须以键值对的形式存在。

我们可以事先在state中定义好一个数据

export default new Vuex.Store({
  state: {
    count: 0
  }
})

由于我们之前已经在Vue实例中通过 store 选项从根组件“注入”到每了一个子组件中,所以我们可以在所有的子组件中通过this.$store 访问store实例中的的内容

Vuex有一种官方推荐的使用方法,因为 Vuex 的状态存储是响应式的,所以Vuex鼓励我们使用Vue的计算属性去从store实例中读取state

<template>
  <div id="app">
    <div class="message">{{ count }}</div>
  </div>
</template>

<script>
export default {
  name: 'App',
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}
</script>

还有的同学会选择在 data 中去读取state中的值

<template>
  <div id="app">
    <div class="message">{{ count }}</div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      count: this.$store.state.count
    }
  }
}
</script>

这样子看上去显示正常,但会有一个隐患,那就是在 data 中获取store实例中的值,不是响应式的,data 中的值存储的只是 created 时候store实例中对应的值的字面量,后续该值发生任何变化,data 中的值都不会发生变化。下面我们可以来看一个例子。

<template>
  <div id="app">
    <div class="message">{{ count }}</div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      count: this.$store.state.count
    }
  },
  mounted () {
    setTimeout(() => {
      this.$store.commit('increment', 10)
    }, 2000)
  }
}
</script>

image.png

可以看到,虽然我们在进入页面两秒后对store实例中的值进行了修改,我们的页面上却并没有跟着变化。

当然官方也没有推荐直接在模板中使用,虽然那样做一样可以达到效果。我也不知道为啥,有知道有什么区别的同学可以在评论区留言。

<template>
  <div id="app">
    <div class="message">{{ this.$store.state.count }}</div>
  </div>
</template>

getters

getters官方讲的比较简单,这里就直接搬过来展示了。

有时候我们需要对取到的值进行计算之后进行展示,例如对列表进行过滤并计数

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

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

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数:

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 也可以接受其他 getter 作为第二个参数:

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

然后我们就可以在组件中直接使用了

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

有时候我们会想给getter 传参,以便让它按照我们不同的需求返回不同的数据。

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

Mutations

Mutation的基本使用

注意:为了防止误解,特注明,Mutatios里的每一个函数都称为一个Mutation,所以当我们说一个Mutation的时候指的是Mutations中的一个函数

前面说了,Vuex给为我们指定了规定的方式去修改仓库里的变量,也就是state里的值,那就是去提交 mutation。因为Vuex 也集成到 Vue 的官方调试工具 devtools,所以我们可以很方便的通过devtools去查看Vuex里的值和相应的变化。

当你安装了vue-devtools之后,可以打开控制台,找到vue标签,点击第二个图标,即可开始Vuex的调试。 image.png

那我们如何去通过Mutation去修改state里的值呢?Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,还可以接收第二个可选参数:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    testMutation (state, obj) {
      state.count = obj.num
    }
  }
})

当我们在组件内调用时,需要通过commit方法去触发mutation,commit方法接受两个参数,第一个为要触发的mutation的名字,第二个参数为我们要传递过去的参数(可选),如果我们要传递多个参数,则可以将多个参数放入一个对象传递过去

this.$store.commit('increment', {num: 10,remarks: '测试' })

当然,我们还有另一种写法,这两种写法除了风格不同,作用完全相同

this.$store.commit({
  type: 'increment',
  num: 10,
  remarks: '测试'
})

Mutation 需遵守 Vue 的响应规则

既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  1. 最好提前在你的 store 中初始化好所有所需属性。
  2. 当需要在对象上添加新属性时,你应该
  • 使用 Vue.set(obj, 'newProp', 123), 或者
  • 以新对象替换老对象。例如,利用对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }

Mutation与devtools

在上面的示例中,我们已经知道如何通过devtools查看store实例里的值,而且我们也说了,通过官方指定的操作方式去修改state的值,会留下操作记录,那操作记录是什么样的呢,下面我们就直接来操作一下。

  state: {
    count: 5
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
  mounted () {
    setInterval(() => {
      this.$store.commit('increment', {
        remarks: '位于首页的计数器'
      })
    }, 5000)
  }

image.png

可以看到,我们每次调用commit都能留下一条操作记录,操作记录里包含了我们触发的Mutation,触发的时间,触发之后的state,我们甚至还可以在其中留下一些‘登记信息’,以便我们不知道这个值是被哪个页面修改的时候进行调试,我们只要单击任意一条记录,便可查看该次触发的详细信息。

并且当我们点击这个小图标时,调试工具包括页面上引用的store实例状态将马上变回此次触发Mutation后store实例的状态,并且我们还可以随时点击最新的记录,以便回到最新的状态。

image.png

不要直接进行赋值操作

看,通过调用commit触发Mutation的方法对于我们的调试来说是不是如此之方便,那如果我们使用直接赋值的方式进行操作会怎么样呢?

  mounted () {
    setInterval(() => {
      this.$store.state.count++
    }, 5000)
  }

image.png

虽然store实例里的值确实被修改了,但却没有留下任何操作记录,我们也无法在调试记录里看到最新的state的值,这对于我们的调试将十分不利,并且对我们的代码语义化也十分不利,它将变得更难以阅读、难以理解。

严格模式

如果你想在项目中仅通过Mutation去修改state里面的值,你可以开启严格模式。在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

开启严格模式,仅需在创建 store 的时候传入 strict: true

const store = new Vuex.Store({
  // ...
  strict: true
})

不要在发布环境下启用严格模式! 严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。

类似于插件,我们可以让构建工具来处理这种情况:

const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

不要在Mutation中进行异步操作 当我们在Mutation中进行异步操作时,Vuex将无法知道我们此次的异步操作将在何时完成,也就无法在操作记录里留下正确的数据,这也就违背了Mutation设计的初衷,下面我们通过实操来看一下

  mutations: {
    increment (state) {
      // 变更状态
      setTimeout(() => {
        state.count++
      })
    }
  }

image.png

可以看到,虽然store实例中的数据可以跟着变化,但我们在每次修改内容的操作记录中却无法记录到正确的值,所以我们只能在Mutation中进行同步操作,那如果我们想要进行异步操作怎么办呢,比如我想把登录的操作放在Vuex中进行怎么办呢?Vuex针对异步操作,也贴心的为我们准备了下一个核心概念——action

actions

注意:为了防止误解,特注明,actions里的每一个函数都称为一个action,所以当我们说一个action的时候指的是action的一个函数

此处官方文档内容比较简单易懂,直接搬过来。

Action 类似于 mutation,不同在于:

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

让我们来注册一个简单的 action:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      setTimeout(() => {
        context.commit('increment')
      }, 2000)
    }
  }
})
this.$store.dispatch('increment')

由于Mutation和Action的触发方式不同,Mutation的名字可以和Action重名

由于action中可以进行任意异步操作,所以我们可以在action中进行异步操作(比如调用api接口)之后再调用commit调用Mutation。

action和Mutation不同的地方在于,Mutation的触发方式为commit,action的触发方式为 dispatch,另外Mutation的第一个参数为state,action的第一个参数为context(上下文对象),相同的是他们都可以用第二个参数接收用户传入的参数。

Vue基础知识巩固之全面了解Vuex,比官方更易懂(下)