Vuex的使用

182 阅读7分钟

Vuex是做什么的?

官方解释: Vuex是一个专为Vue.js应用程序开发的状态管理模式。

  • 它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  • Vuex也集成到Vue的官方调试工具devtools extension,提供了诸如零配置的time-travel调试,状态快照导入导出等高级调试功能。 状态管理到底是什么?
  • 状态管理模式,集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。
  • 其实,可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。
  • 然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。 为什么要使用Vuex?

    如果在一个项目开发中频繁的使用组件传参的方式来同步data的值,一旦项目变得很庞大,管理和维护这些值将是相当棘手的工作。我个人理解是Vuex最大的作用是如果希望把各个页面上的一些公共变量保持一致,然后就可以使用Vuex把这些变量放在一个容器(store) 中作为一个大管家进行管理,比如:
  • 用户的登录状态(token)、用户的信息(头像、名称、地理位置信息)
  • 商品的收藏,购物车的商品 而且这些状态应该是响应式的。

单页面数据图

image.png

  • state,驱动应用的数据源(比如data里的属性);
  • view,以声明方式将 state 映射到视图(针对State的变化,显示不同的信息);
  • actions,响应在 view 上的用户输入导致的状态变化(用户的各自操作,比如click事件)。 但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。
  • 所以我们需要vuex的规范操作来管理状态。

多界面状态管理

Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?

  • 多个试图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
  • 不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态) 也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个试图,但是也有一些状态(状态a/状态b/状态c)属于多个视图想要共同维护的。
  • 状态1/状态2/状态你放在自己的房间中,你自己管理自己用,没问题。
  • 但是状态a/状态b/状态c我们希望交给一个大管家来统一管理。
  • Vuex就是为我们提供这个大管家的工具。

基本使用

store/index.js

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {},
  actions: {},
  modules: {}
})

Home.vue

<template>
  <div class="home">
    <h2>{{ $store.state.count }}</h2>
    <button @click="$store.state.count++">+</button>
    <button @click="$store.state.count--">-</button>
  </div>
</template>

image.png

store/index.js的state中添加属性,然后在Home.vue中调用和修改。测试发现确实可以修改到数据,但在实际开发中,官方不推荐这样直接使用$store.state.count++这种形式直接操作vuex的状态。

Vue状态管理图例

image.png

  • Vue Components是vue组件
  • Mutations :更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
  • State 是vuex中状态的集合
  • Actions与Mutations 类似,经常与后端交互,不同在于:
    • Action 提交的是 mutation,而不是直接变更状态。
    • Action 可以包含任意异步操作。

通过状态图可以知道,如果直接使用$store.state.count++这样修改,那就是直接通过vue component来修改state了。但官方建议说,我们需要先发布一个Action(行为),然后再提交到Mutations,最后再修改State。

为什么呢?

因为vue有一个浏览器插件,叫Devtools,通过这个插件,可以清楚的跟踪到状态的改变,若不按官方给出的顺序来做的话,插件无法检测得到状态的改变,导致在后期维护的调试的时候十分困难。
Devtools只能检测得到同步操作的状态变化,所以开发中异步操作是在Actions中进行,同步操作在Mutations中进行。

Mutations的使用

store/index.js

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  },
  actions: {},
  modules: {}
})

Home.vue

<template>
  <div class="home">
    <h2>{{ $store.state.count }}</h2>
    <button @click="addition">+</button>
    <button @click="substraction">-</button>
  </div>
</template>

<script>
export default {
  name: "Home",
  data() {
    return {};
  },
  methods: {
    addition() {
      this.$store.commit("increment");
    },
    substraction() {
      this.$store.commit("decrement");
    },
  },
};
</script>

在进行操作的时候,Devtools插件可以监控的到状态的变化。

image.png

传递参数

在通过Mutation更新数据的时候,有可能我们希望携带一些额外的参数,而参数被称为是Mutation的载荷(Payload)

store/index.js

//每点击一次就加5
add5count(state, payload) {
      state.count += payload
    }

Home.vue

<button @click="add5count(5)">+5</button>

add5count(count) {
      this.$store.commit("add5count", count);
    },

如果要传递多个参数,那就直接传一个对象。

store/index.js

addOneStu(state, payload) {
      state.students.push(payload)
    }

Home.vue

<button @click="addOneStu">add</button>

addOneStu() {
      const stu = { id: 16, name: "hello7", age: 23 };
      this.$store.commit("addOneStu", stu);
    },

Mutations提交风格

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

add5count(count) {
      //普通的提交封装
      this.$store.commit("add5count", count);

      //特殊的提交封装
      this.$store.commit({
        type: "add5count",
        count,
      });
    },
add5count(state, count) {
      console.log(count)
    },

当Mutations中函数接收参数的时候,两种风格的count是不同的含义。

输出

普通的提交封装

add5count(state, count) {
      console.log(count)
      state.count += count
    },

image.png

特殊的提交封装

当使用特殊的提交封装时,参数不再是count本身,而是payload(载荷),所以要取出真正的count需要用payload.count。

add5count(state, payload) {
      console.log(payload)
      state.count += payload.count
    },

image.png

Mutations类型常量

一个vue文件中有关mutation的方法太多了,常常可能写错,所以可以在store文件夹下定义一个mutation-type.js的常量。

  1. 定义一个常量
export const INCREMENT = 'increment'
  1. 在需要的地方导入并使用
import {
  INCREMENT
} from './mutations-types'

[INCREMENT](state) {
   state.count++
},

定义了常量之后,就算小写的'increment'写错了,在使用大写的INCREMENT也不会报错,以将错就错命名的形式进行下去。

State单一状态树的理解

官方提出单一状态树的概念,用一个对象就包含了全部的应用层级状态,作为一个“唯一数据源”而存在,这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照,在之后的维护和调试过程中,也可以非常方便的管理和维护。如果状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难。

Getters基本使用

getters是store的计算属性,对state的加工,是派生出来的数据。就像computed计算属性一样,getter返回的值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变才会被重新计算。

store/index.js

export default new Vuex.Store({
  state: {
    count: 0
    students: [{
        id: 10,
        name: 'hello1',
        age: 18
      },
      {
        id: 11,
        name: 'hello2',
        age: 20
      },
      {
        id: 12,
        name: 'hello3',
        age: 15
      },
      {
        id: 13,
        name: 'hello4',
        age: 19
      },
      {
        id: 14,
        name: 'hello5',
        age: 21
      },
      {
        id: 15,
        name: 'hello6',
        age: 25
      },
    ]
  },
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  },
  getters: {
    powerCounter(state) {
      return state.count * state.count
    },
    more20stu(state) {
      return state.students.filter(s => s.age > 20)
    }
  },
  actions: {},
  modules: {}
})

Home.vue

<template>
  <div class="home">
    <h2>{{ $store.state.count }}</h2>
    <button @click="addition">+</button>
    <button @click="substraction">-</button>
    <h2>{{ $store.getters.powerCounter }}</h2>
    <h2>{{ $store.getters.more20stu }}</h2>
  </div>
</template>

<script>
export default {
  name: "Home",
  data() {
    return {};
  },
  methods: {
    addition() {
      this.$store.commit("increment");
    },
    substraction() {
      this.$store.commit("decrement");
    },
  },
};
</script>

image.png

在getters中调用getters中的其他函数

比如使用上面的例子,计算超过20岁的学生的个数,在gettters中直接定义另一个函数,带有两个参数。

more20stuLength(state, getters) {
      return getters.more20stu.length
    }

然后再在要使用的地方调用就行。

现在又有一个需求,现在希望可以直接传递一个参数给getters中的函数使用

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

moreAgestu(state) {
      return function (age) {
        return state.students.filter(s => s.age > age)
      }
    }

Vuex的响应式原理

  1. Vue的store的state是响应式的,当state中的数据发生改变时,Vue组件会自动更新。
  2. 响应式需要遵循的规则
    • state的对象需要初始化
    • 如果需要给state中的对象添加新属性的时候,使用以下方式
      a. 使用Vue.set(obj,'newProp',123)
      b. 用新对象替换旧对象
  3. state中的属性都会被加入到响应式系统中,而响应式系统会监听属性的变化,当属性发生变化时,会通知所有界面中用到该属性的地方让界面发生刷新。
  4. 在state中增加一个对象user
user: {
   name: 'wecle',
   number: 123456,
   sex: '男'
},
  1. 在Home.vue中添加按钮和方法对数据进行修改
<h2>{{ $store.state.user }}</h2>
<button @click="updateInfo()">update</button>

updateInfo() {
  this.$store.commit("updateInfo");
},
  1. 在store中添加方法
updateInfo(state) {
  state.user.number = 2333
}
  1. 点击update修改,界面发生刷新,Devtools也监控到了变化

image.png

8.但是当我们想要直接添加一个新属性的时候,比如添加一个address

updateInfo(state) {
  state.user['address'] = 'China'
  // state.user.number = 2333
}
  1. 可以看到虽然Devtools监控到了变化,但是界面没有刷新

image.png

10.若要响应式,应该使用Vue.set()方法

updateInfo(state) {
  // state.user['address'] = 'China'
  Vue.set(state.user, 'address', 'China')
}
  1. 再次点击update就发现已经是响应式的了

image.png

Action的使用

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

  1. 在Actions中添加一个aUpdateInfo函数用定时器模拟异步操作
aUpdateInfo(context) {
  setTimeout(() => {
    context.commit('updateInfo')
  }, 1000)
}

在函数中异步调用Mutation中的方法进行同步操作,Devtools可以监控。

  1. 在Home.vue中调用aUpdateInfo函数
updateInfo() {
  this.$store.dispatch("aUpdateInfo");
},

this.$store.dispatch调用Actions中的方法。

  1. 点击update测试,Devtools成功跟踪到state的变化

image.png

  1. Actions中的方法一样可以传递参数,只需要添加一个payload作为变量就可以了。

  2. actions回调,在异步操作后,成功或者失败都应该会有回调。在actions的方法中返回一个Promise对象。

aUpdateInfo(context) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       context.commit('updateInfo')

       resolve("事件已经完成了")
     }, 1000)
  })
}
  1. 修改aUpdateInfo()方法,获取回调的值。
updateInfo() {
  this.$store.dispatch("aUpdateInfo").then((res) => {
    console.log("已经完成了提交");
    console.log(res);
  });
}
  1. 点击update,打印结果

image.png

Modules的使用

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

基本使用

const moduleA = {
  state: {
    name: "Wecle_2"
  },
  mutations: {
    UpdateName(state, payload) {
      state.name = payload
    }
  },
  actions: {},
  getters: {}
}

const moduleB = {
  state: {},
  mutations: {},
  actions: {},
  getters: {}
}

export default new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

在调用的时候也很简单

<h2>{{ $store.state.a.name }}</h2>

直接可以打印结果

image.png

模块的局部状态

  1. 模块内部的mutation 和 getter,接收的第一个参数是模块的局部状态对象
  2. 模块内部的 action,局部状态是 context.state ,根节点状态则为 context.rootState
  3. 对于模块内部的 getter,第三个参数是根节点状态。
const moduleA = {
  state: () => ({
    count: 0
  }),
  
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
  
  actions: {
    incrementIfOddOnRootSum (context) {
      if ((context.state.count + context.rootState.count) % 2 === 1) {
        context.commit('increment')
        //这个commit只会调用模块中的mutations
      }
    }
  },
  
  getters: {
    //模块中的方法会多一个rootstate参数,可以访问根状态
    doubleCount (state, getters, rootState) {
      console.log(rootState.count) // 获取的是根状态的count
      return state.count * 2
    }
  }
}

action的context

这里可以这样写

actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
        if ((context.state.count + context.rootState.count) % 2 === 1) {
            context.commit('increment')
        }
    }
}

这里属于ES6中对象的解构。

store文件目录组织

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。
  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  3. 异步逻辑都应该封装到 action 里面。 只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

image.png