Vuex 是什么?
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state) 。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
啥是“状态管理模式”?
以一个vue的单文件组件来举例:
为啥使用 Vuex?
当我们开发一个大型的单页面应用时,会出现大量的共享数据,可能很多页面会用到同一个数据,维护起来就比较困难。通过使用vuex对这些共享的数据进行集中化管理,可以使我们的数据更清晰,项目更结构化并且好维护。
(数据就是官网文档里状态的意思,我是这么理解的。)
Vuex在项目中的引入和使用
安装
以下是NPM安装方式,其他的可以瞅瞅官网。
// 注意:不标明版本的时候是安装的最新版本,Vue2.x需要使用vuex3.x版本
npm install vuex --save
npm install vuex@3 --save
引入项目
这里演示的是在main.js中引入,正常使用时一般会以单独的文件写。
import Vue from 'vue';
import Vuex from 'vuex'; // 第一步:引入插件
Vue.use(Vuex); // 第二步:通过全局方法 `Vue.use()` 使用vuex插件
// 第三步:创建一个 store,里边是state对象和mutation...
const store = new Vuex.Store({
// ......
});
new Vue({
store, // 第四步:将状态从根组件“注入”到每一个子组件中
render: h => h(App),
}).$mount('#app');
Vuex的核心概念
State
State就跟咱们单个组件里的data差不多,不过State是全局的。
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0,
},
});
new Vue({
store,
render: h => h(App),
}).$mount('#app');
到这一步,你就可以在每个页面中使用state中定义的数据啦!
怎么使用嘞?(state部分就以咱们上边代码里定义的‘count’来举例)
this.$store.state.count;// 这样就能获取到咱们定义的count了
官方文档说需要通过组件中的计算属性(computed)来访问,但我看着直接访问也没多大区别。
<template>
<div>
<p>{{ count }}</p>
</div>
</template>
<script>
export default {
computed: {
count () {
return this.$store.state.count
}
},
}
</script>
按照官方文档中获取vuex数据的方式接着往下走:
当一个组件中需要用到多个vuex数据时,还像上边代码里那么写,就很麻烦了!
所以Vuex给咱们提供了一个辅助函数(mapState),它可以帮助我们自动生成计算属性的函数,好处就是少写几行代码:
<template>
<div>
<p>{{ count }}</p>
<p>{{ countAlias }}</p>
<p>{{ countPlusLocalState }}</p>
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex';
export default {
data() {
return {
localCount: '哈哈哈',
}
},
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount;
}
}),
}
</script>
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组:
computed: mapState([
'count',
]),
像上边两种情况只考虑了使用Vuex中的数据,但是正常开发中,组件也存在自己的局部计算属性。所以可以使用 对象展开运算符 来混合它们使用。
computed: (
// 你自己的计算属性该咋写就咋写
...mapState({
// 这里边可以参照上边的代码(对象方式、数组方式)
})
),
---------- state结束 ----------
Getter
有些时候原始的 Vuex 数据不能满足咱们的需要,得做一些处理后才能使用,比如对一个数组的数据进行条件过滤。但咱们不能改变原始数据啊,所以Vuex允许我们定义一个getters选项,来做这些操作。这玩意就跟咱们组件中的computed差不多的意思。
定义getter
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
userArray: [
{ name: '张三', age: 40 },
{ name: '李四', age: 20 }
],
},
// 定义getters
getters: {
getUserFil: state => {
return state.userArray.filter(i => i.age < 30);
}
// 可以通过让 getter 返回一个函数,实现给 getter 传参。
getUserFilCopy: (state) => (name) => {
return state.userArray.filter(i => i.name === name);
}
}
});
new Vue({
store,
render: h => h(App),
}).$mount('#app');
访问getter
<template>
<div>
<p>年龄小于30的人是:{{ userFil[0].name }}</p>
<p>张三的年龄是:{{ userFilCopy[0].name }}</p>
</div>
</template>
<script>
export default {
computed: {
// 1. 通过属性访问getter
userFil() {
return this.$store.getters.getUserFil;
},
// 2.通过方法访问getter
userFilCopy() {
return this.$store.getters.getUserFilCopy('张三');
}
},
}
</script>
getter也有一个辅助函数mapGetters,跟state的差不多,访问多个getter时少写点代码。这玩意目前貌似不允许以方法的形式访问getter,没法传参。
<template>
<div>
<p>年龄小于30的人是:{{ userFil[0].name }}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters({
'userFil',
}),
},
}
</script>
Mutation
更改state中数据的时候,不建议直接去改动,官方文档的说法是:
【你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。】
在实际项目中,就我本人经历的项目哈,其实也会有直接更改的情况出现(当然这是不规范的哈)。
所以嘞,这个时候就需要咱们在mutations选项里,定义一些用来处理state中数据的函数。
定义mutation
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
// 不传参数的写法
increment (state) {
state.count++
},
// 荷载(就是要给mutation传参数进去)
incrementCopy (state, num) {
state.count += num;
},
}
});
new Vue({
store,
render: h => h(App),
}).$mount('#app');
提交mutation
<template>
<div>
<p>count:{{ getCount }}</p>
<div>
<button @click="toCommitMutation">提交mutation</button>
<button @click="toCommitMutationCopy">提交mutation(传递参数)</button>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
computed: {
getCount() {
return this.$store.state.count;
},
},
methods: {
toCommitMutation() {
this.$store.commit('increment');
},
// 需要传参的情况
toCommitMutationCopy() {
this.$store.commit('increment', 30);
},
},
}
</script>
官方文档里还有一种风格的提交方式,简单了解下:
store.commit({
type: 'increment', // 这个type就是咱们定义的mutation函数名
amount: 10, // 这里是要传递的参数
})
不出意外,mutation也有一个辅助函数:mapMutations
<template>
<div>
<p>count:{{ getCount }}</p>
<div>
<button @click="toCommitMutation">提交mutation</button>
<button @click="toCommitMutationCopy">提交mutation(传递参数)</button>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex';
export default {
computed: {
getCount() {
return this.$store.state.count;
},
},
methods: {
toCommitMutation() {
// 以下三行代码是同一个作用!
// this.$store.commit('increment');
// this.add();
this.increment();
},
// 需要传参的情况
toCommitMutationCopy() {
// 以下三行代码是同一个作用!
// this.$store.commit('incrementCopy', 30);
// this.addCopy(30);
this.incrementCopy(30);
},
// mapMutations的两种写法
// 1.(对象形式,需要起别名的话就用这个)
...mapMutations({
add: 'increment', // 将 `this.add()` 映射为 `this.$store.commit('increment')`
addCopy: 'incrementCopy'
}),
// 2.(数组形式,不需要起别名就用这个)
...mapMutations([
'increment',
'incrementCopy'
])
},
}
</script>
注意:一条重要的原则就是要记住 mutation 必须是同步函数。
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
以下例子和说明直接用官方文档。
注册action
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。
实践中,我们会经常用到 ES2015 的参数解构来简化代码(特别是我们需要调用 commit 很多次的时候):
actions: {
increment ({ commit }) {
commit('increment')
}
}
分发 Action
Action 通过 store.dispatch 方法触发:
store.dispatch('increment')
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation:
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求
// 然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。
在组件中分发 Action
你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用 async / await,我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
一个
store.dispatch在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
Module
如果我们需要维护的数据量特别大,且存在许多getter、mutation、action,这时候store对象的代码量就很臃肿了,看着也不舒服,不利于维护。
为了解决这个问题,Vuex允许我们将store切割成多个模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
除非你的项目复杂到一定程度,才会出现分模块的情况。
一个简单的store文件结构(不划分模块)
(下边不写action,如果需要异步操作,写法是一样的。)
├── components
│ ├── xxx.vue
│ └── ...
├── view
│ ├── xxx.vue
│ └── ...
├── App.vue
├── main.js
└── store
├── index.js # 导出 store 的地方
├── state.js # 根级别的 state
├── getters.js # 根级别的 getter
└── mutations.js # 根级别的 mutation
a: 将之前咱们写在main.js中的代码抽离出来:(可以在src目录下创建一个store文件夹)
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
const store = new Vuex.Store({
state,
getters,
mutations,
});
export default store;
const state = {
count: 0,
userArray: [
{ name: '张三', age: 40 },
{ name: '李四', age: 20 }
],
};
export default state;
const getters = {
getFileData: (state) => {
return state.userList.filter(i => i.age > 20);
},
getFileDataCopy: (state) => (name) => {
return state.userList.filter(i => i.name === name);
}
};
export default getters;
const mutations = {
increment (state) {
state.count ++;
},
incrementCopy (state, num) {
state.count += num;
},
};
export default mutations;
b. 修改main.js文件
import Vue from 'vue';
import store from './store/index.js'; // vuex
new Vue({
store,
render: h => h(App),
}).$mount('#app');
一个简单的分模块store就实现了!