529 vuex:State, Getters, Mutations ,Actions, Modules
一、Vuex是做什么的?
(1)官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
(2)状态管理到底是什么?
状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。
其实,你可以简单的将其看成 把多个组件、界面共享的变量全部存储在一个对象里面。
然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。【挂载到Vue.prototype】
那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?
(3)等等,如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?
当然可以,只是我们要先想想VueJS带给我们最大的便利是什么呢?没错,就是响应式。
如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。
不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了。
二、管理什么状态呢?
(1)但是,有什么状态时需要我们在多个组件间共享的呢?
如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。
比如用户的登录状态、用户名称、头像、地理位置信息等等。
比如商品的收藏、购物车中的物品等等。
这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的(待会儿我们就可以看到代码了,莫着急)。
(2)OK,从理论上理解了状态管理之后,让我们从实际的代码再来看看状态管理。
毕竟,Talk is cheap, Show me the code.(来自Linus)
我们先来看看但界面的状态管理吧.
三、单界面的状态管理
(1)我们知道,要在单个组件中进行状态管理是一件非常简单的事情
什么意思呢?我们来看下面的图片。
(2)这图片中的三种东西,怎么理解呢?
State:不用多说,就是我们的状态。(你姑且可以当做就是data中的属性)
View:视图层,可以针对State的变化,显示不同的信息。(这个好理解吧,响应式)
Actions:这里的Actions主要是用户的各种操作,点击、输入等等,会导致状态的改变。
(3)写点代码,加深理解:
看下右边的代码效果, 肯定会实现吧?
四、单界面状态管理的实现
在这个案例中,我们有木有状态需要管理呢?没错,就是个数counter。
counter需要某种方式被记录下来,也就是我们的State。
counter目前的值需要被显示在界面中,也就是我们的View部分。
界面发生某些操作时(我们这里是用户的点击,也可以是用户的input),需要去更新状态,也就是我们的Actions
这不就是上面的流程图了吗?
五、多界面状态管理
(1)Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?
多个视图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态)
(2)也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个视图,但是也有一些状态(状态a/状态b/状态c)属于多个视图共同想要维护的
状态1/状态2/状态3,你放在自己的房间中,你自己管理自己用,没问题。
但是状态a/状态b/状态c,我们希望交给一个大管家来统一帮助我们管理!!!
没错,Vuex就是为我们提供这个大管家的工具。
(3)全局单例模式(大管家)
我们现在要做的就是将共享的状态抽取出来,交给我们的大管家,统一进行管理。
之后,你们每个试图,按照我规定好的规定,进行访问和修改等操作。
这就是Vuex背后的基本思想。
六、Vuex状态管理图例
一起在来看一副官方给出的图片
在Vue component dispatch Ation(触发行为),commit mutation(提交修改,devtools监听修改),改变state。
七、简单的案例
我们还是实现一下之前简单的案例
首先,我们需要在某个地方存放我们的Vuex代码:
这里,我们先创建一个文件夹store,并且在其中创建一个index.js文件
在index.js文件中写入如下代码:
// 我写的
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
strict: true,
state: {
counter: 11
},
mutations: {
increment(state) {
state.counter++
},
decrement(state) {
state.counter--
}
}
})
export default store
八、挂载到Vue实例中
其次,我们让所有的Vue组件都可以使用这个store对象
来到main.js文件,导入store对象,并且放在new Vue中
这样,在其他Vue组件中,我们就可以通过this.$store的方式,获取到这个store对象了
// 我写的
import Vue from 'vue'
import App from './App.vue'
import store from './store/index.js'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
九、使用Vuex的count
(1)好的,这就是使用Vuex最简单的方式了。
(2)我们来对使用步骤,做一个简单的小结
1)提取出一个公共的store对象,用于保存在多个组件中共享的状态
2)将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
3)在其他组件中使用store对象中保存的状态即可
通过 this.$store.state.属性 的方式来访问状态
通过 this.$store.commit('mutation中方法') 来修改状态
(3)注意事项
我们通过提交mutation的方式,而非直接改变store.state.count。
这是因为Vuex可以更明确的追踪状态的变化,所以不要直接改变store.state.count的值。
// 我写的
<template>
<div id="app">
<h1>{{$store.state.counter}}</h1>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
// couter: 0
}
},
components: {},
methods: {
increment() {
this.$store.commit('increment')
},
decrement() {
this.$store.commit('decrement')
},
},
};
</script>
<style>
* {
font-size: 30px;
/* font-weight: 900; */
}
button {
margin-right: 20px;
font-size: 20px;
}
</style>
十、Vuex核心概念
Vuex有几个比较核心的概念:
State
Getters
Mutations
Actions
Modules
我们对它进行一一介绍.
十一、State单一状态树
(1)Vuex提出使用单一状态树, 什么是单一状态树呢?
英文名称是Single Source of Truth,也可以翻译成单一数据源。
(2)但是,它是什么呢?我们来看一个生活中的例子。
OK,我用一个生活中的例子做一个简单的类比。
我们知道,在国内我们有很多的信息需要被记录,比如上学时的个人档案,工作后的社保记录,公积金记录,结婚后的婚姻信息,以及其他相关的户口、医疗、文凭、房产记录等等(还有很多信息)。
这些信息被分散在很多地方进行管理,有一天你需要办某个业务时(比如入户某个城市),你会发现你需要到各个对应的工作地点去打印、盖章各种资料信息,最后到一个地方提交证明你的信息无误。
这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作(需要大量的各个部门的人力来维护,当然国家目前已经在完善我们的这个系统了)。
(3)这个和我们在应用开发中比较类似:
如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难。所以Vuex也使用了单一状态树来管理应用层级的全部状态。
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。
十三、Getters基本使用
有时候,我们需要从store中获取一些state变异后的状态,比如下面的Store中:
获取学生年龄大于20的个数。
我们可以在Store中定义getters
十四、Getters作为参数和传递参数
如果我们已经有了一个获取所有年龄大于20岁学生列表的getters, 那么代码可以这样来写
getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数.
比如上面的案例中,我们希望根据ID获取用户的信息
getters.js
/*
getters.js被导入的时候,直接导入的名字的文件名getters,
import getters from './getters',
类似的还有:
import mutations from './mutations'
import actions from './actions'
import moduleA from './modules/moduleA'
不要写成:
export default {
getters: {}
}
*/
export default {
powerCounter(state) {
return state.counter * state.counter
},
more20stu(state) {
return state.students.filter(s => s.age > 20)
},
// 【第一、二个参数是state、getters,固定的,不能传第三个参数。】
more20stuLength(state, getters) {
return getters.more20stu.length
},
// 返回函数,最后的大括号还可以省略。
moreAgeStu(state) {
// return function (age) {
// return state.students.filter(s => s.age > age)
// }
return age => {
return state.students.filter(s => s.age > age)
}
}
}
十五、Mutation状态更新
(1)Vuex的store状态的更新唯一方式:提交Mutation
(2)Mutation主要包括两部分:
字符串的事件类型(type);
一个回调函数(handler),该回调函数的第一个参数就是state。
mutation的定义方式:
(3)通过mutation更新
十六、Mutation传递参数
(1)在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数
(2)参数被称为是mutation的载荷(Payload)
Mutation中的代码:
十七、Mutation提交风格
上面的通过commit进行提交是一种普通的方式
Vue还提供了另外一种风格, 它是一个包含type属性的对象
十八、Mutation响应规则
(1)Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.
(2)这就要求我们必须遵守一些Vuex对应的规则:
提前在store中初始化好所需的属性.
当给state中的对象添加新属性时, 使用下面的方式:
方式一: 使用Vue.set(obj, 'newProp', 123)
方式二: 用新对象给旧对象重新赋值
(3)我们来看一个例子:
当我们点击更新信息时, 界面并没有发生对应改变.
(4)如何才能让它改变呢?
查看下面代码的方式一和方式二
都可以让state中的属性是响应式的.
十九、Mutation常量类型 – 概念
(1)我们来考虑下面的问题:
在mutation中, 我们定义了很多事件类型(也就是其中的方法名称).
当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多.
方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.
(2)如何避免上述的问题呢?
在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutation事件的类型.
我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.
(3)具体怎么做呢?
我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量.
定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称.
二十、Mutation同步函数
(1)通常情况下, Vuex要求我们Mutation中的方法必须是同步方法.
主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照.
但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.
(2)比如我们之前的代码, 当执行更新时, devtools中会有如下信息: 图1
(3)但是, 如果Vuex中的代码, 我们使用了异步函数: 图2
(4)你会发现state中的info数据一直没有被改变, 因为他无法追踪到.
(5)So, 通常情况下, 不要再mutation中进行异步的操作
二十一、Action的基本定义
(1)我们强调, 不要再Mutation中进行异步操作.
但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?
Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.
(2)Action的基本使用代码如下:
(3)context是什么?
context是和store对象具有相同方法和属性的对象.
也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等.
但是注意, 这里它们并不是同一个对象, 为什么呢? 我们后面学习Modules的时候, 再具体说.
(4)这样的代码是否多此一举呢?
我们定义了actions, 然后又在actions中去进行commit, 这不是脱裤放屁吗?
事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了.
二十二、Action的分发
在Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch。【组件中,用dispatch。在actions中,用commit。】
同样的, 也是支持传递payload
二十三、Action返回的Promise
前面我们学习ES6语法的时候说过, Promise经常用于异步操作.
在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.
OK, 我们来看下面的代码:
二十四、认识Module
(1)Module是模块的意思, 为什么在Vuex中我们要使用模块呢?
Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理.
当应用变得非常复杂时,store对象就有可能变得相当臃肿.
为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutation、action、getters等
(2)我们按照什么样的方式来组织模块呢?
我们来看左边的代码
二十五、Module局部状态
上面的代码中, 我们已经有了整体的组织结构, 下面我们来看看具体的局部模块中的代码如何书写.
我们在moduleA中添加state、mutations、getters
mutation和getters接收的第一个参数是局部状态对象
二十六、Actions的写法
(1)actions的写法呢? 接收一个context参数对象
局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
(2)如果getters中也需要使用全局的状态, 可以接受更多的参数
二十七、项目结构
当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰.
main.js
import Vue from 'vue'
import App from './App'
import store from './store'
Vue.config.productionTip = false
new Vue({
el: '#app',
store, // 挂载到Vue上后,其他所有的组件都可以使用 $store 了
render: h => h(App)
})
App.vue
<template>
<div id="app">
<h5>----------App内容: modules中的内容----------</h5>
<h5>{{$store.state.a.name}}</h5>
<button @click="updateName">修改名字</button>
<h5>{{$store.getters.fullname}}</h5>
<h5>{{$store.getters.fullname2}}</h5>
<h5>{{$store.getters.fullname3}}</h5>
<button @click="asyncUpdateName">异步修改名字</button>
<h5>----------App内容: info对象的内容是否是响应式----------</h5>
<h5>{{$store.state.info}}</h5>
<button @click="updateInfo">修改信息</button>
<h5>----------App内容----------</h5>
<h5>{{$store.state.counter}}</h5>
<button @click="addition">+</button>
<button @click="subtraction">-</button>
<button @click="addCount(5)">+5</button>
<button @click="addCount(10)">+10</button>
<button @click="addStudent">添加学生</button>
<h5>----------App内容: getters相关信息----------</h5>
<h5>{{$store.getters.powerCounter}}</h5>
<h5>{{$store.getters.more20stu}}</h5>
<h5>{{$store.getters.more20stuLength}}</h5>
<h5>{{$store.getters.moreAgeStu(12)}}</h5>
<hr />
<h5>----------Hello Vuex内容----------</h5>
<hello-vuex />
</div>
</template>
<script>
import HelloVuex from "./components/HelloVuex";
import { INCREMENT } from "./store/mutations-types";
export default {
name: "App",
components: {
HelloVuex,
},
data() {
return {
message: "我是App组件",
};
},
// computed: {
// more20stu() {
// return this.$store.state.students.filter(s => s.age > 20)
// }
// },
methods: {
addition() {
this.$store.commit(INCREMENT);
},
subtraction() {
this.$store.commit("decrement");
},
addCount(count) {
// payload: 负载
// 1.普通的提交封装
// this.$store.commit('incrementCount', count)
// 2.特殊的提交封装
this.$store.commit({
type: "incrementCount",
count,
});
},
addStudent() {
const stu = { id: 114, name: "alan", age: 35 };
this.$store.commit("addStudent", stu);
},
updateInfo() {
// this.$store.commit('updateInfo')
// this.$store.dispatch('aUpdateInfo', {
// message: '我是携带的信息',
// success: () => {
// console.log('里面已经完成了');
// }
// })
this.$store.dispatch("aUpdateInfo", "我是携带的信息").then((res) => {
console.log("里面完成了提交");
console.log(res);
});
},
updateName() {
this.$store.commit("updateName", "lisi");
},
asyncUpdateName() {
this.$store.dispatch("aUpdateName");
},
},
};
</script>
<style>
* {
margin: 8px 0 8px 3px;
padding: 0;
}
</style>
index.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
import moduleA from './modules/moduleA'
// 1.安装插件
Vue.use(Vuex)
// 2.创建对象
const state = {
counter: 1000,
students: [
{ id: 110, name: 'why', age: 18 },
{ id: 111, name: 'kobe', age: 24 },
{ id: 112, name: 'james', age: 30 },
{ id: 113, name: 'curry', age: 10 }
],
info: {
name: 'kobe',
age: 40,
height: 1.98
}
}
const store = new Vuex.Store({
state,
mutations,
actions,
getters,
modules: {
a: moduleA
}
})
// 3.导出store独享
export default store
// 对象的解构
const obj = {
name: 'why',
age: 18,
height: 1.88,
address: '洛杉矶'
}
const { name, height, age } = obj;
mutations.js
import { INCREMENT } from "./mutations-types";
export default {
// 下面这种写法是 ES6 风格的“计算属性名”,计算属性名:使用 [赋值表达式] 形式。因为Javascript对象Object的key是string类型的,用中括号访问的方式会把里面的表达式转换成string。 文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Object_initializer#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7%E5%90%8D
[INCREMENT](state) {
state.counter++
},
// 形参state代表store对象的state
decrement(state) {
state.counter--
},
incrementCount(state, payload) {
// console.log(count);
state.counter += payload.count
},
addStudent(state, stu) {
state.students.push(stu)
},
updateInfo(state) {
state.info.name = 'coderwhy'
// 错误的代码: 不能在这里进行异步操作
// setTimeout(() => {
// state.info.name = 'coderwhy'
// }, 1000)
// state.info['address'] = '洛杉矶' // 该方式做不到响应式
// Vue.set(state.info, 'address', '洛杉矶') // 该方式是响应式
// delete state.info.age // 该方式做不到响应式
// Vue.delete(state.info, 'age') // 该方式是响应式
}
}
actions.js
export default {
// context: 上下文
// aUpdateInfo(context, payload) {
// setTimeout(() => {
// context.commit('updateInfo')
// console.log(payload.message);
// payload.success()
// }, 1000)
// },
aUpdateInfo(context, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('updateInfo')
console.log(payload)
resolve('1111111')
}, 1000)
})
}
}
getters.js
/*
getters.js被导入的时候,直接导入的名字的文件名getters,
import getters from './getters',
类似的还有:
import mutations from './mutations'
import actions from './actions'
import moduleA from './modules/moduleA'
不要写成:
export default {
getters: {}
}
*/
export default {
powerCounter(state) {
return state.counter * state.counter
},
more20stu(state) {
return state.students.filter(s => s.age > 20)
},
// 【第一、二个参数是state、getters,固定的,不能传第三个参数。】
more20stuLength(state, getters) {
return getters.more20stu.length
},
moreAgeStu(state) {
// return function (age) {
// return state.students.filter(s => s.age > age)
// }
// 最后的大括号还可以省略。
return age => {
return state.students.filter(s => s.age > age)
}
}
}
mutations-types.js
// 这种写法,导入的时候,就要 import {INCREMENT}。如果是export default,导入的时候,就 XXX,不用{}包裹
export const INCREMENT = 'increment'
moduleA.js
export default {
state: {
name: 'zhangsan'
},
mutations: {
updateName(state, payload) {
state.name = payload
}
},
getters: {
fullname(state) {
return state.name + '11111'
},
fullname2(state, getters) {
return getters.fullname + '2222'
},
fullname3(state, getters, rootState) {
return getters.fullname2 + rootState.counter
}
},
actions: {
aUpdateName(context) {
console.log(context);
setTimeout(() => {
context.commit('updateName', 'wangwu')
}, 1000)
}
}
}
index_copy.js
import Vue from 'vue'
import Vuex from 'vuex'
import {
INCREMENT
} from './mutations-types'
// 1.安装插件
Vue.use(Vuex)
// 2.创建对象
const moduleA = {
state: {
name: 'zhangsan'
},
mutations: {
updateName(state, payload) {
state.name = payload
}
},
getters: {
fullname(state) {
return state.name + '11111'
},
fullname2(state, getters) {
return getters.fullname + '2222'
},
fullname3(state, getters, rootState) {
return getters.fullname2 + rootState.counter
}
},
actions: {
aUpdateName(context) {
console.log(context);
setTimeout(() => {
context.commit('updateName', 'wangwu')
}, 1000)
}
}
}
const store = new Vuex.Store({
state: {
counter: 1000,
students: [
{ id: 110, name: 'why', age: 18 },
{ id: 111, name: 'kobe', age: 24 },
{ id: 112, name: 'james', age: 30 },
{ id: 113, name: 'curry', age: 10 }
],
info: {
name: 'kobe',
age: 40,
height: 1.98
}
},
mutations: {
// 方法
[INCREMENT](state) {
state.counter++
},
decrement(state) {
state.counter--
},
incrementCount(state, payload) {
// console.log(count);
state.counter += payload.count
},
addStudent(state, stu) {
state.students.push(stu)
},
updateInfo(state) {
state.info.name = 'coderwhy'
// 错误的代码: 不能在这里进行异步操作
// setTimeout(() => {
// state.info.name = 'coderwhy'
// }, 1000)
// state.info['address'] = '洛杉矶'
// Vue.set(state.info, 'address', '洛杉矶')
// 该方式做不到响应式
// delete state.info.age
// Vue.delete(state.info, 'age')
}
},
actions: {
// context: 上下文
// aUpdateInfo(context, payload) {
// setTimeout(() => {
// context.commit('updateInfo')
// console.log(payload.message);
// payload.success()
// }, 1000)
// },
aUpdateInfo(context, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('updateInfo');
console.log(payload);
resolve('1111111')
}, 1000)
})
}
},
getters: {
powerCounter(state) {
return state.counter * state.counter
},
more20stu(state) {
return state.students.filter(s => s.age > 20)
},
more20stuLength(state, getters) {
return getters.more20stu.length
},
moreAgeStu(state) {
// return function (age) {
// return state.students.filter(s => s.age > age)
// }
return age => {
return state.students.filter(s => s.age > age)
}
}
},
modules: {
a: moduleA
}
})
// 3.导出store独享
export default store
// 对象的解构
const obj = {
name: 'why',
age: 18,
height: 1.88,
address: '洛杉矶'
}
const { name, height, age } = obj;