官方解释:
Vuex是一个专为Vue.js应用程序开发的
状态管理模式
。它采用集中式存储管理
应用的所有组件的状态,并且以相应的规则保证状态以一种可预测的方式发生变化。Vuex也集成到Vue的官方调试工具,提供了诸如零配置的time-travel、状态快照导入导出等高级调试功能。
那么状态管理、集中式存储管理到底是什么呢?
其实,我们可以简单将其看做是多个组件共享的变量
全都存放到一个对象里面,然后将这个对象放入到顶层的Vue实例中,让其他组件可以使用。而且Vuex还是响应式的状态管理.
那么先来了解下,假如在要在Vue中放置一个全局对象,能做到吗?当然能,但是这个对象不是一个响应式的对象。
假如已经导入了Vue相关的依赖
const obj = {
name: "wlh",
age: 21
}
Vue.prototype.obj = obj // 在Vue中放置一个全局对象
Vue.component('aaa', { // 由于其他组件都是继承自Vue的,都能拿到这个对象,但是这个对象不是响应式的
this.obj.name
this.obj.age
})
那么Vuex就是为了解决这样不是全局响应式变量
的问题存在的。
最常见的就是用户登录后的一些信息如:用户名称、头像、token
等全局性的一些变量,还比如商品的收藏,购物车中的商品等等,而且还是实现的响应式的。
像常见的父子组件通信类的一些变量就没有必要去放到Vuex中进行管理了,直接父子通信方式传递数据就行了。
看一幅图:
其中State其实就是指我们的状态(变量数据,如在vue组件中定义的数据),然后会放到View,也就是视图上面去展示出来。再通过某个事件我们可以控制State(数据的状态),不停循环。
代码说明:
<template>
<div id="app">
<h2>{{counter}}h2>
<button @click="counter++">+button>
<button @click="counter--">-button>
div>
template>
<script>
export default {
name: 'App',
data() {
return {
counter: 0
}
}
}
script>
上述代码块中, data()中定义的变量就是我们的State状态,然后页面上将 counter变量渲染了出来,当我点击+、-
按钮时触发事件然后更改data()中定义的变量,也就是更改State状态,形成一个循环圈。
假如在App.vue组件中用到了另一个组件,想要两个组件都能共用一个变量。当然这种简单的场景使用父子组件间的Props传值完全可以实现,这里只是为了说明如何进行全局性的状态管理。
npm install vuex --save
vuex是一个插件,我们需要使用 Vue.use
来使用插件。
目录下新建一个store
的文件夹(约定俗成的命名),创建一个index.js
文件
import Vue from 'vue'
import Vuex from 'vuex'
// 1.安装插件
Vue.use(Vuex)
// 2.创建对象
const store = new Vuex.Store({
// 保存状态
state:{
counter: 1000
}
})
// 3.导出store对象
export default store
在main.js中,导入store,并且给vue实例使用
import Vue from 'vue'
import App from './App'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
render: h => h(App)
})
放置到vuex的state对象中的变量,在其他组件中都可以使用 $store.state.xxx
的方式调用它们
比如上面我们在store的state中存放了一个counter为1000的变量,然后我们在一个组件中使用它们:
两种都可以使用
<template>
<div>
<h2>{{$store.state.counter}}h2>
<h2>{{counter}}h2>
div>
template>
<script>
export default {
name: 'HelloVuex',
data() {
return {
// 这种方式有一个坏处:因为data()是每次vue组件实例化完后返回的,这个counter如果不进行维护,那么只会在实例化组件的时候返回一次,后面如果 store.state.counter变化了,这里是不能及时响应的。
counter: this.$store.state.counter
}
}
}
script>
<style>
style>
npm run dev
跑起来项目,如果没有跑起来报错了,那么大多都是因为版本的问题:
如果你的vue版本是 2.X ,将vuex升到 3.X.X 就能够解决,使用命令方式安装或者直接在package.json中改版本然后 npm install
的方式都可以
npm install --save [email protected]
如果你的vue版本是 3.X ,将vuex升到 4.X.X 就能够解决
npm install --save [email protected]
页面上正常显示我们在vuex的state中定义的状态:
访问是没问题了,那么如果我要修改vuex中state中的变量时该怎么做呢?
可能我们都理想当然这样用:
App.vue
<template>
<div id="app">
<h2>---------------App.vueh2>
<h2>{{$store.state.counter}}h2>
<button @click="$store.state.counter++">+button>
<button @click="$store.state.counter--">-button>
<h2>---------------HelloVuexh2>
<hello-vuex>hello-vuex>
div>
template>
<script>
import HelloVuex from './components/HelloVuex'
export default {
name: 'App',
components: {
HelloVuex
}
}
script>
HelloVuex.vue
<template>
<div>
<h2>{{$store.state.counter}}h2>
<h2>{{counter}}h2>
div>
template>
<script>
export default {
name: 'HelloVuex',
data() {
return {
counter: this.$store.state.counter
}
}
}
script>
在HelloVuex中第二个counter是调用的data()中的,然后data()函数中的counter只在实例化时返回了一次,后面并没有维护,那它始终都是store.state.counter的初始值。
按照vuex中定义好的规范,我们在修改vuex中的state时不能随意更改,比如: $store.state.counter++,这样子是不符合vuex中的规范的。我们需要在触发vue组件中的事件后,commit到Mutations中进行修改。后面我们要使用一个浏览器插件 DevTools
,为了能够让我们使用这个插件更好地进行跟踪是谁修改了 state的状态,方便差错及快速定位,修改state时要在Mutations中执行。
我们先把DevTools安装到谷歌浏览器上面。
大多数人的谷歌浏览器应该没办法打开谷歌商店的,可以去网上下载一下。极简插件下载,下载完解压一下然后直接将解压完的文件拖入到谷歌的扩展程序中。
整完后重启一下浏览器,再次打开vue程序,选择好vue插件的页面。
那么开始定义真正对store中的state操作的部分,也就是 Mutations中的定义:
mutations中定义的方法中,参数默认会传递一个 state
变量,这个state就是store中的state对象。
const store = new Vuex.Store({
state:{
counter: 1000
},
mutations:{
increment(state){ // state是固定的,要写
state.counter++;
},
decrement(state){
state.counter--;
}
}
})
我们在组件中要对vuex中的state修改时,就需要去调用上面的 mutations中定义的规定好的方法来执行。
<button @click="add()">+button>
<button @click="minus()">-button>
<script>
export default {
name: 'App',
methods: {
add(){
this.$store.commit('increment')
},
minus(){
this.$store.commit('decrement')
}
}
}
script>
调用 $store.commit方法,传入mutations中相应的方法名称,直接调用方法来操作store中的state.
此图中说明了vue组件在访问和修改vuex中的state时的完整流程,其中Actions走不走都可以,但是一定要牢记:修改vuex中state的状态时,一定是经过Mutations的
Single Source of Truth : 单一数据源。只创建一个Vuex实例对象,任何访问vuex中state的状态时,都需要经过这定义的唯一一个vuex实例对象(单一的数据源)。便于我们管理和维护。
类似vue中的计算属性,计算缓存(一个计算属性被获取到后,会产生一个缓存数据)和及时更新数据,只不过处于不同的对象中。
有一场景:state中有定义一个counter的属性,但是在使用时都想要这个counter的平方值,那么我可以定义一个getters
在定义vuex中的index.js文件中
getters:{
powerCounter(state){
return state.counter * state.counter
}
}
那么如何调用呢?
$store.getters.powerCounter
直接调用即可,这也是一个属性
<h2>---------------App.vue counter的平方h2>
<h2>{{$store.getters.powerCounter}}h2>
getters中的参数传参:
假如state中有一个数组,我们要筛选出>20的数的数据,并且还要得到数量。我们怎么做呢?
const store = new Vuex.Store({
// 保存状态
state:{
arrs:[1,2,30,40,23,25]
},
getters:{
getArr(state){
return state.arrs.filter(x => x > 20)
},
// 此方法中 state虽然没有用到,但是一定不能省略,否则就直接报错了
// 方法中,state和getters可以命名为其他名称,但是实际上第一个参数就是 指的state,第二个参数就是指的 getters引用,与名称无关
getArrLength(state, getters){
return getters.getArr.length
}
}
})
那么假如我并不确定筛选大于多少的数据怎么办呢?需要外界给传入一个参数
那么,我们可以:定义一个getters,这个getters不返回一个属性,而是返回一个函数,可以让外界调用并且传入参数
getActiveArrLen(state){
return age => {
return state.arrs.filter(x => x > age)
}
}
如何调用?
筛选出 > 30 的数据
<h2>{{$store.getters.getActiveArrLen(30)}}h2>
当需要更改Vuex中store状态时,唯一的修改方式就是:提交 Mutation
Mutation主要包括两部分:
调用时给传入参数,可以被称之为 是 mutations的 负载(payload)
比如
mutatsions:{
// increment就是事件的类型
// 整个方法体就是一个 回调函数。,而且第一个参数就是 Vuex的 state
increment(state){
state.counter++
}
}
既然第一个参数默认就是state,那么假如有其他的参数我该如何传给它呢?
第一个是默认的state,后面可以加上我们需要接收的参数即可。
const store = new Vuex.Store({
// 保存状态
state:{
counter: 1000
},
mutations:{
// 这里记得接收参数 count
incrementCount(state, count){
state.counter += count
}
}
})
调用时传入参数:
<button @click="addCounter(10)">+10button>
<script>
export default {
name: 'App',
methods: {
addCounter(count){
// 调用 commit到mutations中,记得传入参数
this.$store.commit('incrementCount', count)
}
}
}
script>
载荷(负载)同时也支持传入对象类型的参数:
addCounter(count){
const stu = {id:1, name: "wlh"}
this.$store.commit('incrementCount', stu)
}
除了通过 代码中的 commit方式进行提交(普通方式)外,Vuex还提供了一种包含type属性的对象方式提交。
addCounter(count){
// 普通提交
this.$store.commit('incrementCount', count)
// 特殊提交
this.$store.commit({
type: 'incrementCount',
count
})
}
上面两种方式都可以进行提交,那么两种方式的提交有什么不同的呢?打印下日志看下
incrementCount(state, count){
console.log(count)
}
也就是说,特殊方式的提交mutations中接收到的是一个 payload对象,这里写为 payload更易理解
incrementCount(state, payload){
console.log(payload)
}
实际开发中,我们在mutations中定义的方法名称和在vue组件中使用commit提交时的名称很可能不小心写错产生问题,那么我们可以统一管理这些名称。
在store目录中创建一个 mutations-types.js文件,这个文件中定义我们用到的名称:
export const UPDATESTU = 'updateStu'
...
在vue组件中使用:
import {UPDATESTU} from './store/mutations-types' // 导入
export default {
name: 'App',
methods: {
update(){
this.$store.commit(UPDATESTU) // 提交时直接使用即可
}
}
}
那么在vuex的mutations中同样也要使用同一个常量:
import {UPDATESTU} from './mutations-types'
const store = new Vuex.Store({
...
mutations:{
// 直接定义即可
[UPDATESTU](state){
}
}
})
通常情况下,Vuex要求我们Mutation中的方法必须是同步方法,原因
Vuex的state中的state是响应式的,当state中的数据发生变化时,Vue组件会自动更新,这就要求我们必须遵守一些Vuex对应的规则:
state中定义的变量都会被加入到响应式系统中,响应式系统会监听属性的变化,当属性发生变化时,会通知界面中所有用到该属性的地方,让界面发生刷新。
如果是给state中的对象新增新的属性时,它不是响应式的,那么我们就需要使用到上述的的两种方式来使它自动刷新。
这里使用的vue2,动态新增属性时不能做到响应式,需要进行Vue.set或其他方式。但是在高本版中的vue中不会存在这种情况,需要注意一下
const store = new Vuex.Store({
// 保存状态
state:{
info:{
name:'wlh',
age: 21
}
},
mutations:{
updateStu(state){
// 动态新增属性,不能做到响应式
//state.info['address'] = 'sdfjkldsfs'
// 使用Vue.set,把这个属性增加到 响应式系统中
Vue.set(state.info, 'address', '北京市')
}
}
})
Vue.delete删除属性,能够做到响应式
Vue.delete(state.info, 'age')
我们前面说到不要在mutation中进行异步操作,但是某些情况下,我们确实希望在Vuex中进行一些异步操作,比如网络请求,必然是异步的操作,该怎么处理呢?
Action类似于Mutation,但是是用来代替Mutation进行异步操作的。
先来回忆一下上面的图:
因为要异步操作state的状态,那么我们需要增加一个Actions环节,先由vue组件dispatch到Actions中,由Actions去commit到Mutations中,然后Mutations对state进行修改
定义actions:
const store = new Vuex.Store({
mutations:{
updateStu(state){
// 处理
state.xxx = 'xxx'
}
},
// 定义actions
actions:{
// 需要用到一个 context上下文,这个context就是我们的 store对象
aUpdateInfo(context){
setTimeout(() => {
// 函数回调中, commit到 mutations的方法中,由 mutations中的方法对state进行状态更新
context.commit('updateStu')
}, 1000);
}
}
})
既然定义好的 actions中已经 commit到了 mutations中,那么在vue的组件中调用时,不用再去commit到mutations中了,直接dispatch到actions中的方法中即可。
<button @click="update">修改button>
<script>
export default {
name: 'App',
methods: {
update(){
// 调用转发到 指定的actions中
this.$store.dispatch('aUpdateInfo')
}
}
}
script>
与mutations一样,actions中也可以接收一些参数,传入参数时支持普通传入和对象传入。
actions:{
aUpdateInfo(context, payload){
setTimeout(() => {
console.log(payload)
context.commit(UPDATESTU)
}, 1000);
}
}
那么当actions中的异步方法执行完成后,如何给一个回调呢?
那就需要用到我们之前学过的Promise了,给异步操作做一个包装,使代码的可读性更高。
在actions中当被访问到时,直接给返回一个 Promise对象,然后后续的回调处理交给调用者。
actions:{
aUpdateInfo(context, payload){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(payload)
context.commit(UPDATESTU)
resolve('1234')
}, 1000);
})
}
}
也就是当我们执行dispatch后actions那里会返回一个Promise对象,然后交个调用者处理回调
methods: {
update(){
this.$store.dispatch('aUpdateInfo', 'payload')
.then(res => {
console.log('方法回调完毕')
console.log('参数是' + res)
})
}
}
modules指的是vuex对象中的模块。当应用变得非常复杂时,store的state可能变得非常臃肿,为了解决这个问题,Vuex允许我们将store分割成模块(module),而每个模块中又有自己的 state、mutations、actions、getters等(简单来讲,就是类似于一种树结构,能够无限套子集)
定义module
// 创建对象
const moduleA = {
state: {
name: 'hhh'
},
mutations:{
updateName(state, payload){
state.name = payload
}
}
}
const store = new Vuex.Store({
modules:{
a: moduleA
}
})
定义了一个名称为a的模块,那么vue组件中如何使用呢?
<h2>{{$store.state.a.name}}h2>
实际上,我们定义的模块是有一个子模块,访问时需要从root级父模块中的state中寻找。
那么访问子模块中的 mutations,该怎么访问呢?
<button @click="updateName">修改名字button>
<script>
export default {
name: 'App',
methods: {
updateName(){
this.$store.commit('updateName', 'ls')
}
}
}
script>
可以看到,直接commit即可,它会先去root级模块中找,找不到再去子模块中去找。所以:所有模块中的mutations定义的名称不要重复
定义一个getters
const moduleA = {
state: {
name: 'hhh'
},
getters:{
fullname(state){
return state.name + '123'
}
}
}
访问其实和之前一样,这些都是全局性的
<h2>{{$store.getters.fullname}}h2>
那么不免会遇到,当子module中想要访问root级中的一些state时怎么办?假如root级state中有一个counter属性
getters:{
fullname(state, getters, rootState){
return state.name + '123' + rootState.counter
}
}
前面说到getters中的参数,第一个是state,第二个getters,那么第三个就是 rootState(随意写的名称),rootState就是root级模块中的state。须知:mutations、getters都是全局性的,无论你是定义在哪个模块中
actions与mutations、getters有些异同
const moduleA = {
state: {
name: 'hhh'
},
mutations:{
updateName(state, payload){
state.name = payload
}
},
actions:{
aUpdateName(context){
console.log(context)
setTimeout(() => {
// 这里的上下文就不是 store对象了,而是当前的一个子模块对象
context.commit('updateName', 'zs');
}, 1000);
}
}
}
在子模块的actions中,执行commit时,只会提交到自己模块中的mutations中
访问actions时还是直接访问即可,所以再次提醒,无论是mutations、getters、actions中的方法名称都不要重复了。
updateName(){
this.$store.dispatch('aUpdateName')
}
我们看一下打印的context信息:
是一个对象类型,有getters,rootGetters,rootState,state等属性。
那么对对象结构
比较熟悉的兄弟可能就产生了一种想法,既然这个接收的context是一个对象类型,那我能不能直接给他结构成为几个对象呢,当然可以.
// 将context 结构为几个 参数,注意:是按照名称分配的,跟顺序无关
aUpdateName({state, commit, rootState}){
setTimeout(() => {
commit('updateName', 'zs');
}, 1000);
}
需要注意到一点:有些属性root级模块是没有的,比如:rootState,root级模块本身就是root,它已经没有上一个父级了
上面的所有的代码都是写到了一个index.js文件中,实际项目中我们会进行导入导出模块来将这些进行抽离。让我们的项目结构更加清晰。
root级目录中state可以这样抽离一下:
const state = {
name: 'wlh'
}
const store = new Vuex.Store({
state
})
root级目录中mutations可以抽离为文件,然后导入这个文件即可。
import {UPDATESTU} from './mutations-types'
export default {
[UPDATESTU](state){
state.info.name = "ls"
}
}
导入使用
import mutations from './mutations'
const store = new Vuex.Store({
mutations
})
actions和getters同理。
在store目录下新建文件夹modules,比如有一个 cart的module,就创建一个 cart.js
export default {
state: {
name: 'hhh'
},
mutations,
getters:{
fullname(state, getters, rootState){
return state.name + '123' + rootState.counter
}
},
actions:{
aUpdateName(context){
console.log(context)
setTimeout(() => {
context.commit('updateName', 'zs');
}, 1000);
}
}
}
在index.js中直接导入使用
import Cart from './modules/cart'
const store = new Vuex.Store({
modules:{
Cart
}
})