文章目录
一、Vuex介绍
1、单界面的状态管理
我们先看一段简单的代码
<template>
<div id="app">
<h2>{
{counter}}</h2>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
counter: 0
}
},
methods: {
increment() {
this.counter++;
},
decrement() {
this.counter--;
}
}
}
</script>
<style></style>
我们看一下上边的代码,有没有状态需要管理呢?有,就是变量counter。
- counter需要某种方式被记录下来,也就是我们的State。
- counter目前的值需要被显示在界面中,也就是我们的View部分。
- 界面发生某些操作时(我们这里是按钮的点击,也可以是用户的input),需要去更新状态,也就是Actions
vuex官网上给了一幅图来解释 State、View、Actions的关系
Vue已经帮我们做好了单个界面的状态管理(状态只在一个组件中使用),就像上边展示的那样。
如果有一些状态(状态a/状态b/状态c),多个组件想要共同维护,应该怎么办呢?
- 多个试图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
- 不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态)
那么Vuex就诞生啦,Vuex就是用来解决多组件状态共享的问题的。
2、Vuex是什么
官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
当遇到多个组件需要共享状态时,传参的方法对于多层嵌套的组件将会非常繁琐。因此,我们可以把组件的共享状态抽取出来,以一个全局单例模式进行管理。在这种模式下,任何组件都能获取状态或者触发行为!
我们也可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。然后,将这个对象放在顶层的Vue实例中,那么多个组件就可以共享这个对象中的所有变量属性了。
如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?就像下面的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<div id="app">
<cpn></cpn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
//由于所有的组件对象都是 Vue的实例,所以我们可以在 Vue.prototype上定义需要共享的变量
Vue.prototype.shareObj = {
name: 'webchang',
age: 18
}
//创建一个组件
Vue.component('cpn', {
template: '<div>我是cpn组件</div>',
created() {
console.log(this);
console.log(this.shareObj);//试图访问共享变量 shareObj
}
})
let vm = new Vue({
el: "#app",
data: {
}
});
</script>
</body>
</html>
结果发现我们可以访问到shareObj变量
我们再看一下打印的cpn组件的this(当前组件实例),看看它的原型链,如图所示:
看起来好像还不错~ 但是我们要先想想Vue带给我们最大的便利是什么呢?就是响应式。上边的代码是不能保证响应式的。那如果再封装实现一个对象保证它里面所有的属性做到响应式不就行了吗?当然也可以,只是自己封装可能稍微麻烦一些。Vuex就是一个现成的工具,在多个组件间共享状态的插件。
3、Vuex用来管理什么状态
- 比如用户的登录状态、用户名称、头像、地理位置信息等等。
- 比如商品的收藏、购物车中的物品等等。
这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的。如果应用程序够简单,可能不需要使用 Vuex。
二、Vuex的安装
安装:npm install vuex --save
在一个模块化的打包系统中,必须显式地通过 Vue.use() 来安装 Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
三、Vuex基本使用
我用vue-cli创建了一个项目来演示
我们需要在某个地方存放我们的Vuex代码。
(1)先创建一个文件夹store,并且在其中创建一个index.js文件,在index.js文件中写入如下代码。
import Vue from 'vue'
import Vuex from 'vuex'
//1.安装插件,安装插件的时候会去调用该插件的install方法
Vue.use(Vuex)
//2.创建对象
const store = new Vuex.Store({
state: {
counter:100
},
mutations: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
}
},
})
//3.导出
export default store
(2)为了让所有的Vue组件都可以使用这个store对象,我们来到main.js文件,导入store对象,并且放在new Vue中。这样,在其他Vue组件中,我们就可以通过 this.$store
的方式,获取到这个store对象了。
import Vue from 'vue'
import App from './App'
import store from "./store"
new Vue({
el: '#app',
store,
render: h => h(App)
})
(3)在其它组件中使用store对象中保存的状态即可
- 通过
this.$store.state
属性的方式来访问状态 - 通过
this.$store.commit('mutation中方法')
来修改状态
例如我们在App.vue中使用
<template>
<div id="app">
<h2>{
{$store.state.counter}}</h2>
<button @click="add">+</button>
<button @click="sub">-</button>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
}
},
methods: {
add() {
this.$store.commit('increment');
},
sub() {
this.$store.commit('decrement');
}
}
}
</script>
<style></style>
我们不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。 这样使得我们可以方便地跟踪每一个状态的变化。状态可以被跟踪指的是我们在浏览器中安装一个vue的插件 Vue.js devtools。这个工具可以帮助我们记录每一次store中状态的变化。如图所示
注意:通常情况下, Vuex要求我们Mutation中的方法必须是同步方法
- 主要的原因是当我们使用上边那个浏览器插件devtools时, 可以devtools可以帮助我们捕捉mutation的快照
- 如果Mutation是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成。但是某些情况下我们必须进行异步操作,例如网络请求,此时我们需要在 actions中进行处理,请看下文。
四、Vuex的核心概念
1、state
单一状态树。
Vuex提出使用单一状态树, 什么是单一状态树呢?英文名称是Single Source of Truth(SSOT),也可以翻译成单一数据源。
这也意味着,每个应用将仅仅包含一个 store 实例。
- 如果你的状态信息是保存到多个store对象中的,那么之后的管理和维护等等都会变得特别困难。
- 所以Vuex也使用了单一状态树来管理应用层级的全部状态。
- 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。
2、getters
有时候我们需要从 store 中的 state 中派生出一些状态,Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
getter 接受 state 作为其第一个参数
const store = new Vuex.Store({
state: {
students: [
{
id: 1, name: 'webchang', age: 19},
{
id: 2, name: 'webchang1', age: 20},
{
id: 3, name: 'webchang2', age: 21},
{
id: 4, name: 'webchang3', age: 22}
]
},
getters: {
getStu(state) {
//我们在这里定义的一个个函数就是 getter
return state.students.filter(item => {
return item.age > 20;
});
}
})
可以通过属性的形式访问这些值 $store.getters.xxx
<h2>{
{$store.getters.getStu}}</h2>
getter 也可以接受第二个参数 getters 用来访问其它的 getter
const store = new Vuex.Store({
//...
getters: {
getStu(state) {
return state.students.filter(item => {
return item.age > 20;
});
},
getStuLength(state, getters) {
return getters.getStu.length;
}
}
})
getter默认是不能传递参数的, 如果希望传递参数, 那么只能让getter本身返回另一个函数
const store = new Vuex.Store({
//...
moreAgeStu(state) {
return function (age) {
return state.students.filter(item => item.age > age);
}
}
})
<h2>把年龄作为参数传递进去:{
{$store.getters.moreAgeStu(19)}}</h2>
3、mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数
传递参数
(1)我们可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload)
this.$store.commit('increment', 10)
// ...
mutations: {
increment (state, payload) {
state.count += payload
}
}
(2)如果有很多参数需要传递,这个时候,可以通过对象的形式传递。
this.$store.commit('increment', {
num:10,name:'webchang'})
commit提交风格
Vue还提供了另外一种 commit 提交风格:对象风格的提交方式
addCount(count) {
//1.普通的提交风格
// this.$store.commit('increment', count);
//2.特殊的提交风格
this.$store.commit({
type:'increment',
count
})
}
此时mutation中的处理方式是将整个commit的对象作为payload使用
mutations: {
//...
incrementCount(state, payload) {
state.count += payload.count;
}
}
使用常量替代 mutation 事件类型
我们来考虑下面的问题:
- 在mutation中, 我们定义了很多事件类型(也就是其中的方法名称)。
- 当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多。
- 方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况。
如何避免上述的问题呢?
- 可以使用常量替代Mutation事件的类型.
- 我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然
具体怎么做呢?我们可以创建一个文件: mutation-types.js
, 并且在其中定义我们的常量
// mutation-types.js
export const INCREMENT = 'increment';
// store.js
import Vuex from 'vuex'
import {
INCREMENT } from './mutation-types'
const store = new Vuex.Store({
state: {
... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[INCREMENT] (state) {
// mutate state
}
}
})
// App.vue
// ...
<script>
import {
INCREMENT} from "./store/mutations-types";
export default {
name: 'App',
data() {
return {
}
},
methods: {
add() {
this.$store.commit(INCREMENT);
}
}
}
</script>
4、actions
我们尽量不要在Mutation中进行异步操作,否则状态的改变无法被跟踪。但是某些情况,我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的。这个时候怎么处理呢? 用 action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
下面是一个简单的action
const store = new Vuex.Store({
state: {
info: {
name: 'webchang',
age: 18
}
},
mutations: {
updateInfo(state) {
state.info.name = 'webchang';
}
},
actions: {
asyncUpdateInfo(context) {
context.commit('updateInfo');
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。 但是注意,context 对象不是 store 实例本身
在组件中分发 Action
(1)在组件中使用如下方式进行分发(dispatch)调用actions中的方法。
this.$store.dispatch('asyncUpdateInfo')
(2)也可以传递参数:
this.$store.dispatch('asyncUpdateInfo', '我是携带的信息')
// 以载荷形式分发
this.$store.dispatch('asyncUpdateInfo', {
amount: 10
})
// 以对象形式分发
this.$store.dispatch({
type: 'asyncUpdateInfo',
amount: 10
})
在Action中对传递过来的参数进行接收
//...
actions: {
asyncUpdateInfo(context,payload) {
console.log(payload);
context.commit('updateInfo');
// 也可以继续把数据传递到 mutation中
// context.commit('updateInfo',payload);
}
}
(3)Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
我们可以在action中返回一个Promise,在store.dispatch后边添加then方法进行处理
actions:{
asyncUpdateInfo(context) {
return new Promise(((resolve, reject) => {
setTimeout(() => {
context.commit('updateInfo');
resolve('action 执行完成');
},1000)
}))
}
}
在组件中进行如下处理
this.$store
.dispatch('asyncUpdateInfo')
.then(data => {
console.log(data);
})
5、modules
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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 的状态
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
对于模块内部的 getter,根节点状态会作为第三个参数暴露出来
const moduleA = {
state: {
name: 'webchang'
},
mutations: {
updateName(state, payload) {
state.name = payload
}
},
getters: {
fullName(state) {
return state.name + ' hello'
},
fullName2(state, getters) {
//通过第二个参数 getters 访问其它 getter
return getters.fullName + ' world!';
},
fullName3(state, getters, rootState) {
//通过第三个参数 rootState 访问根节点的状态
return getters.fullName2 + rootState.counter;
}
},
actions: {
asyncUpdateName(context,payload) {
setTimeout(() => {
context.commit('updateName',payload)
}, 1000)
}
}
}
const store = new Vuex.Store({
state: {
counter:100
},
modules: {
a: moduleA,
}
})
五、Vuex中的响应式
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
前提是我们必须遵守一些Vuex对应的规则:提前在 store 中初始化好所需的属性。
代码演示,提前在state中定义了一个info对象,info对象中有两个属性name和age
const store = new Vuex.Store({
state: {
info: {
//初始就有的属性都会被加入到响应式系统中,而响应式系统会监听属性的变化
//当属性发生变化时会通知所有界面中用到该属性的地方,让界面发生刷新
name: '灰太狼',
age: 40
}
},
mutations: {
updateInfo(state) {
//这个可以做到响应式,因为 info对象初始就有 name属性
state.info.name = 'webchang';
}
}
})
(1)如果我们想要在info对象中新增一个属性,如何确保新增的属性也是响应式的?我们可以使用 Vue.set()方法
Vue.set()方法向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。
mutations: {
updateInfo(state) {
//该方式做不到响应式,info对象初始没有address属性
//state.info['address'] = '北京';
//该方法可以做到响应式
Vue.set(state.info,'address','北京');
}
}
(2)如果我们想要删除一个属性,并确保删除后能触发更新视图,怎么办?可以使用 Vue.delete()方法
mutations: {
updateInfo(state) {
// 该方式做不到响应式
// delete state.info.age;
// 该方法可以做到响应式
Vue.delete(state.info,'age');
}
}
六、项目结构
如果我们把上边的所有代码都放在一个 js 文件里,那么这个文件就太大了,不好管理,所以当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰,只需将 action、mutation 和 getter 分割到单独的文件。
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
七、资料
本博客演示代码的下载链接:https://webchang.lanzous.com/i7l5ykmlswd 密码:bxpi
本博客介绍的内容有限,大家可以通过官网继续学习