vuex-核心概念 | 青训营笔记

90 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天

Vuex有几个比较核心的概念:

  • State: 保存共享状态,存放状态相关信息
  • Getters: 类似计算属性
  • Mutation: 状态更新
  • Action: 进行异步操作
  • Module: 划分模块,针对不同模块进行相关数据的保存

State 单一状态树

STATE属性里面有个mapState的辅助函数

State: 保存共享状态,存放状态相关信息

在一个项目里只建一个store

Vuex提出使用单一状态树, 什么是单一状态树呢? 英文名称是Single Source of Truth,也可以翻译成单一数据源

如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难。 所以Vuex也使用了单一状态树来管理应用层级的全部状态。

单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。

Getters 类似计算属性 需要从store中获取一些state变异后的状态时使用

和mutations不同的应该是,他是拿来用,不改原来的样子

在mutation里定义的操作会改变state里面的属性值,但getter里定义的方法只是对数据进行了一番处理,并没有改变state里面的属性值。

getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数

    <h2>{{ $store.getters.powerCounter }}</h2>
    <h2>{{ $store.getters.more20stu }}</h2>
    <h2>{{ $store.getters.more20stuLength }}</h2>
    <h2>{{ $store.getters.moreAgeStu(18) }}</h2>

  getters: {
    powerCounter (state) {
      return state.counter * state.counter
    },
    more20stu(state) {
      return state.students.filter( s => s.age >= 20)
    },
    // getters也可以作为参数转入
    // 但getters里的方法只能接收两个参数:state和getters
    more20stuLength(state, getters) {
      return getters.more20stu.length
    },
    // 想要自己给定比较的age,可以返回一个函数,将参数传入
    moreAgeStu(state) {
      return age => {
        return state.students.filter( s => s.age > age)
      }
    }
  },

Mutation 状态更新

Vuex的store状态的更新唯一方式:提交Mutation

Mutation主要包括两部分:

  • 字符串的事件类型(type) (例如 increment)
  • 一个回调函数(handler),该回调函数的第一个参数就是state。
// mutation的定义方式:
  mutations: {
    // 方法  默认参数state  自动传入state 
    increment(state) {
      state.counter++
    }
  },

// 通过mutation更新
// 在app里
  methods: {
    addition() {
      this.$store.commit("increment")
      // 使用commit方法,传入方法名
      // 跟踪每一次的提交的状态
    }
  },

Mutation传递参数

在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数

参数被称为是mutation的载荷(Payload)

但是如果参数不是一个呢? 比如我们有很多参数需要传递. 这个时候, 我们通常会以对象的形式传递, 也就是payload是一个对象. 这个时候可以再从对象中取出相关的信息.

// Mutation中的代码:
  mutations: {
    incrementCounter(state, count) {
      // mutation的使用与事件处理函数非常相似,都具有类型和回调函数
      // (类似methods,不过获取state中的变量不是this.变量名,而是state.变量名)。
      state.counter += count
    },
    addStudent(state, stu) {
      state.students.push(stu)
    }
  },

  // 在app里的代码:
    <button @click="addCount(5)">+5</button>
    <button @click="addCount(10)">+10</button>
    <button @click="addStudent">添加学生</button>

  methods: {
    addCount(count) {
      this.$store.commit("incrementCounter", count)
    },
    addStudent() {
      // 有多个参数,通过对象同一传递
      // payload:负载  参数被称为是mutation的载荷(Payload)
      const stu = { id : 104, name : "alban", age : 26}
      this.$store.commit("addStudent", stu)
    }
  }

Mutation提交风格

上面的通过commit进行提交是一种普通的方式。 Vue还提供了另外一种风格, 它是一个包含type属性的对象

    addCount(count) {
      // 1. 普通的提交风格
      // 提交的只是值
      // this.$store.commit("incrementCounter", count)

      // 2. 特殊的提交风格
      // 将整个对象提交过去
      this.$store.commit({
        type : "incrementCounter",  // type就是mutations里的方法名
        count   // counter : counter  ES6
      })
    }

Mutation中的处理方式是将整个commit的对象作为payload使用, 所以代码没有改变, 依然如下

    incrementCounter(state, payload) {
      // mutation的使用与事件处理函数非常相似,都具有类型和回调函数
      // (类似methods,不过获取state中的变量不是this.变量名,而是state.变量名)。

      // 普通提交
      // console.log(count);
      // state.counter += count

      // 特殊提交
      // 此时count可以写成payload
      console.log(payload);
      state.counter += payload.count
    }

Mutation响应规则

前排提醒,本节讲述的方法在Vue3已经废弃,Vue3可以直接在mutations正常赋值

Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.

这就要求我们必须遵守一些Vuex对应的规则: 提前在store中初始化好所需的属性.

当给state中的对象添加新属性时, 使用下面的方式:

  • 方式一: 使用Vue.set(obj, 'newProp', 123)
  • 方式二: 用新对象给旧对象重新赋值
    <h2>------------App组件--Mutations使用--info对象的内容是否响应式-------------</h2>
    <button @click="updataInfo">修改info信息</button>
    <h2>{{ $store.state.info }}</h2>
        updataInfo() {
      this.$store.commit("updataInfo")
    }

    mutations代码   
        updataInfo(state) {
      // state.info.name = "abc"

      // 要添加属性
      // 这种方法不是响应式的
      // state.info['address'] = '多伦多'
      // 
      Vue.set(state.info, 'address', '多伦多')

      // 删除属性
      // delete state.info.age
      Vue.delete(state.info, 'age')
    }

state里的每个对象的初始化属性都有一个watcher,一开始就定义的属性会被加入vue的响应式系统, 之后就会通过 DEP->[watcher]观察属性的变化, (每个属性对应一个dep,dep对应很多watcher,dep存放wather的集合 内部响应式系统:dep监听属性的变化-观察者模式,数据变化后看那些地方需要根据数据变化进行刷新界面 dep有个数组,里面放着很多watcher,通知对应watcher进行页面变化)

state里一开始就定义的属性都会被加入到响应式系统中,而响应式系统会监听属性的变化,当属性发生变化时,会通知所有界面中用到该属性的地方,让界面发生刷新

每个属性都有自己的一个depency数组,里面装着不同的watcher,watcher则是对应视图中每一个有调用这个属性的地方

后增加的属性没有加入响应式系统,所以无法响应,界面没有刷新 要想实现响应式,需要使用Vue.set(要增加的属性的对象,"属性名“,属性值) 删除用Vue.delete(要删除的属性的对象,"属性名")

Mutation常量类型

概念

问题: 在mutation中, 我们定义了很多事件类型(也就是其中的方法名称). 当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多. 方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.

如何避免? 一种很常见的方案就是使用常量替代Mutation事件的类型. 将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.

具体怎么做? 创建一个文件: mutation-types.js, 并且在其中定义我们的常量. 定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称.

代码

-----mutation-types.js
export const INCREMENT = 'increment'
这样导出时要注意,导入时要用对象(普通导出)

-----index.js
    // ['函数名字'](){} //在对象中可以这样定义方法
    // 在ES6中,把属性名用[ ]括起来,则括号中就可以引用提前定义的变量。
    [INCREMENT](state) {
      state.counter ++
    },

-----app.vue
  import HelloWorld from './components/HelloWorld.vue';  // 这种方式只能是export default导出
  import {
    INCREMENT,
  }from './store/mutations-types'  // 普通导出要这样导入
this.$store.commit(INCREMENT)

Mutation同步函数

Vuex要求我们Mutation中的方法必须是同步方法

在mutations里进行异步操作,看起来是修改了state, 但是devtools无法跟踪该步操作,使得记录的state数据是操作之前的,导致出现错误

Action

总结:state(存放数据),getter(处理数据), mutations(修改数据), actions(接收异步数据)

概念

Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.

异步操作要在actions里进行,然后提交commit到mutataions,在mutations里对state进行修改 在app里,需要dispatch到actions, actions可以传递参数payload,它的第一个参数为context,可以理解为store

context是和store对象具有相同方法和属性的对象,可以通过context去进行commit相关的操作, 也可以获取context.state等。但是注意, 这里它们并不是同一个对象, 为什么呢? 我们后面学习Modules的时候, 再具体说

基本使用

app  
// 进行异步操作
      this.$store.dispatch('aUpdateInfo', '我是payload')
----------

  actions: {
    aUpdateInfo(context, payload) {  // context: 上下文 可以可理解为store  可以传递参数
      setTimeout(() => {
        // context.state.info.age = 35  不能这样,因为只能在mutations里修改state
        context.commit('updateInfo',payload)
        console.log(payload);
      }, 1000)
    }
  },

这里打开devtools后,添加信息按钮报错,解决方法: devtools导致报错的,把插件里Plugin settings的legacy action开启就好了

如果app想要知道异步操作是否完成,要怎么做?

一般认为 commit成功了就是异步操作完成

  1. 可以把回调函数放到payload中传递 不够优雅
// app.js
      this.$store.dispatch('aUpdateInfo', {
        messege : '我是携带的信息',
        success () {
          console.log('异步操作成功');
        }
      })
  1. 更加优雅的方式 dispatch执行发送.then执行回调,

在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.

Promise经过dispatch中转,在app里调用then方法

// Promise经过dispatch中转,在app里调用then方法
// app组件
      this.$store
        .dispatch('aUpdateInfo2', 'payload携带的信息')
        .then( res => {
          console.log(res);
          console.log('异步操作成功');
        })
// index.js
    aUpdateInfo2(context, payload) {
      // 2. actions返回Promise,经过dispatch,在组件中使用then方法
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          context.commit('updateInfo')
          console.log(payload);
          resolve('返回Promise方法')
        }, 1000)
      })
    }

Module 模块 (套娃)

概念

为什么在Vuex中我们要使用模块呢? Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理. 当应用变得非常复杂时,store对象就有可能变得相当臃肿. 为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutations、actions、getters等

我们按照什么样的方式来组织模块呢?

代码:

// 组件
<h2>{{ this.$store.state.a.messege }}</h2>
// modules里的a会被放入到state里,所以使用时:this.$store.a.messege
    updateMessege(){
      this.$store.commit('updateMessege', '书籍是人类进步的阶梯')
      // 一般模块里的mutations方法的名字不要和store里重复 直接commit,提交统一commit 
    }
// index.js
const moduleA = {
  state : {
    messege : '大大怪将军'
  },
  mutations: {
    updateMessege(state, payload) {
      state.messege = payload
    }
  },
  actions : {
    aUpdateMessege(context) { // 只commit模块内部的mutations,这里的context相当于自己模块
      setTimeout(() => {
        context.commit('updateMessege', '小心超人')
      }, 1000)
    }
  },
  getters : {
    fullmessege(state){
      return state.messege + '1111'
    },
    fullmessege2(state, getters) {
      return getters.fullmessege + '2222'
    }
    // 如果想要获取store的state
    // 在模块内可以有第三个参数rootState
    fullmessege3(state, getters, rootState) {
      return getters.fullmessege2 + ' ' + rootState.counter
    }
  }
}

actions还有另一种写法 因为这里的context多了两个属性:rootState,rootGetters 所以如果actions需要store的数据,可以使用 ES6中有个对象解构的知识

// 在actions中
actions : {
  isEqual({state, commit, rootState}) {
    if(state.counter == rootState.counter)
      commit('equal')
  }
}

// 对象解构
const obj = {
  name : 'yuli',
  age : '20',
  address : '洛杉矶'
}
const {name, age} = obj
解构赋值
解构对象的时候,名称一定要与对象里面的名称一样
顺序可以替换,也可以少分配
数组就得按顺序了,对象是按key来的

// 完整写法是name:name,左边name是匹配模式,右边是变量名,简写就是直接name,
// 如果想改变量名就name:na,一定记住结构时冒号左边是匹配模式,右边是要赋值的变量,
// 具体可以看阮一峰es6

注意:

  1. modules里的a会被放入到state里,所以组件使用时:this.$store.a.messege
  2. 一般模块里的mutations方法的名字不要和store里重复 直接commit,提交统一commit
  3. 如果模块内想要获取store的state,在模块内getters可以有第三个参数rootState,还有rootGetters
  4. actions的参数context相当于自己模块,只能commit模块内部的mutations
  5. dispatch时,模块的actions名字也不要重复
  6. 使用模块的mutations,actions,getters 和store一样,this.$store.方法名