Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能
为什么要使用vuex?记得在vue的props中说过,vue的数据是单向流动的,每一次更改数据都必须通过父组件的方法更改,但是因为数据是单向流动的,当我们出现这么一个场景,两个同级组件都需要同一个数据怎么办,这个时候就只能将父组件当做容器进行传递数据了,可能这个数据就只有那两个组件需要,大家可以试想一下当我们的应用有几个页面需要一些公有的数据层级非常深的时候,传递数据可以说是非常的复杂,这时候我们肯定会考虑一个管理数据的容器了?这就是为什么要使用vuex了!
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
一个简单的基于vuex的简单计数器小例子
<body>
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
// new 一个VuexStore对象
const store = new Vuex.Store({
state: {
count: 0
},
// 要想改变state必须通过mutations方法改
mutations: {
increment: state => state.count++,
decrement: state => state.count--
}
})
new Vue({
el: '#app',
computed: {
count() {
// 通过计算属性绑定store到对应组件上
// 需要哪一个组件绑定哪一个组件
return store.state.count
}
},
methods: {
// 要想调用mutations必须使用store.commit对应的mutation函数
increment() {
store.commit('increment')
},
decrement() {
store.commit('decrement')
}
}
})
</script>
</body>
注释已经比较详细了
state
单一状态树,意思就是管理应用所有状态的对象只有一个,每一个Vue实例也只能包含一个store实例
你会发现获取Store的数据是通过计算属性获取的,因为store的更新会带动使用了store中对应数据的组件的更新,所以每一次store中的数据更新,计算属性如果检测到需要的数据更新都将会重新求值重而重新渲染界面
-
但是我们还有一种更简单的注入store数据的方法,直接通过
store
选项绑定到vue实例上,并且会分发给所以此实例的子组件,相当于将store
绑定到了这一组件树上了
mapState辅助函数,可以快速的帮我们绑定store数据到组件上
computed:Vuex.mapState({
// key为组件内访问的名称,后面是一个函数接收store中的state我们可以直接返回
// 我们需要的数据,或者通过state的数据计算出我们需要的数据
mcount: state => state.count
}),
<div id="app">
<p>{{ mcount }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
上面的绑定方式提供了一种简写方式
computed:Vuex.mapState({
// key为组件内访问的名称,后面是一个函数接收store中的state我们可以直接返回
// 我们需要的数据,或者通过state的数据计算出我们需要的数据
// mcount: state => state.count
// 上面的写法可以简写为
// 传入字符串count就等于 --> state => state.count
mcount:'count'
}),
data() {
return {
tcount:88
}
},
computed: Vuex.mapState({
// key为组件内访问的名称,后面是一个函数接收store中的state我们可以直接返回
// 我们需要的数据,或者通过state的数据计算出我们需要的数据
// mcount: state => {
// // 这里的this是window对象,大概是因为箭头函数的缘故
// console.log(this);
// return state.count
// }
// 上面的写法可以简写为
// 传入字符串count就等于 --> state => state.count
// mcount:'count'
// 也可以使用函数的写法,可以获取当前作用域下的this
mcount(state) {
return state.count + this.tcount
}
}),
如果我们传递键值对是相同的,类似于state的key是什么我们就以什么key绑定到组件上,还可以进行进一步简写
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
computed: Vuex.mapState(['count']),
大家会发现上面的这种写法其实已经可以说是替换掉了所有的computed了,难道vuex使用就有这种缺点吗?不不不,vue官方提供了一种解决方案帮助我们将mapState返回的对象融入到computed对象中(为什么会占用整个computed,因为mapState返回的就是一个对象),那么大家肯定有一种想法了,看下面的例子
computed: {
state:Vuex.mapState(['count'])
},
不知道大家有没有这种想法,但是很遗憾就是这个方法不行,所以我们只能老老实实用vue官方的方法(低版本浏览器就不要想了)
computed: {
ctcount(){
// 22次平方,Python数学运算符
return this.tcount ** 22;
},
// 混入mapState
// 这是展开运算符,但是是展开对象的,而不是数组
...Vuex.mapState(['count'])
},
<div id="app">
<p>混合成功: {{ctcount}}</p>
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
关于state部分介绍到此
Getter
想像一种场景,我们组件需要的数据是通过state中的数据计算出来的,这时候简单的计算还好说如果是复杂的过滤呢?每一次使用这个数据或者很多个组件需要过滤后的数据那么就必须重复写许多的过滤方法,或者定义一个公共的方法
computed: {
doneTodosCount () {
// 如果其他组件需要要么抽离出去一个公共函数,要么直接copy代码
return this.$store.state.todos.filter(todo => todo.done).length
}
}
其实无论那种解决的办法都不是特别的理想,所以vuex允许我们在store中定义getter属性(可以认为是store的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
const store = new Vuex.Store({
state: {
count: 0
},
// 要想改变state必须通过mutations方法改
mutations: {
increment: state => state.count++,
decrement: state => state.count--
},
getters:{
// 每一个getter接收state为第一个参数
compuCount(state){
// 这样我们就计算除了一个不知道的值
return state.count ** 3 / 87 * 4 % 2;
}
}
})
那么我们是把getter定义出来了,但是我们需要怎么访问这个getter呢?
不可能是这样吧...Vuex.mapState(['compuCount'])
,这样就直接报错了,那么vuex提供了三种方式访问Store的getter
- 通过属性访问
<body>
<div id="app">
<p>{{compuCount}}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
// new 一个VuexStore对象
const store = new Vuex.Store({
state: {
count: 2
},
// 要想改变state必须通过mutations方法改
mutations: {
increment: state => state.count++,
decrement: state => state.count--
},
getters:{
testGetter(state){
return state.count ** 2;
},
// 每一个getter接收state为第一个参数
// getter还可以接收其他的getter作为第二个参数
compuCount(state,otherGetters){
// 第二个参数包含了所有的getters
console.log(otherGetters);
// 这样我们就计算除了一个不知道的值
return state.count ** 10 / 87 * 4 % 5;
}
}
})
new Vue({
el: '#app',
// 绑定store
store,
data() {
return {
}
},
computed: {
compuCount(){
return this.$store.getters.compuCount;
}
},
methods: {
// 要想调用mutations必须使用store.commit对应的mutation函数
increment() {
store.commit('increment')
},
decrement() {
store.commit('decrement')
}
}
})
</script>
</body>
注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
- 通过方法访问
你可以让你定义的getter返回一个函数,通过传入不同的参数返回不同的结果
<body>
<div id="app">
<p>{{powerCount}}</p>
<button @click="add">+</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state) => { state.count++ },
minus: (state) => { state.count-- }
},
getters: {
powerCount: (state) => (num) => {
console.log(state.count,num);
return state.count ** num;
}
}
})
new Vue({
// 必须绑定store,不然mapState报一些奇怪的错误
store,
computed: {
...Vuex.mapState(['count']),
powerCount(){
return store.getters.powerCount(this.count);
}
},
methods: {
add() {
store.commit('add');
},
minus() {
store.commit('minus')
}
},
}).$mount("#app");
</script>
</body>
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
- mapGetters辅助函数
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性,跟mapState的使用方法完全一致
computed: {
...Vuex.mapState(['count']),
// 如果你不想使用getter的name绑定到store上
// 可以{yourName:'powerCount'},传入一个对象
// 你定义什么名字,就绑定什么名字到实例上
// 实测无法绑定返回函数的getter
...Vuex.mapGetters(['pCount'])
},
Mutation
mutation一句话就是改变state的唯一途径(后面的异步action也是通过最后调用mutation改变state的)
跟redux一样,触发mutation我们可以给mutation传参,叫做payload(载荷)
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add">+10</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state, num, num2) => {
// 10 undefined
console.log(num, num2);
num = num || 1;
return state.count += num;
},
minus: (state) => { state.count-- }
},
getters: {
powerCount: (state) => (num) => {
console.log(state.count, num);
return state.count ** num;
},
pCount(state) {
return state.count ** state.count;
}
}
})
new Vue({
// 必须绑定store,不然mapState报一些奇怪的错误
store,
computed: {
...Vuex.mapState(['count']),
// 如果你不想使用getter的name绑定到store上
// 可以{yourName:'powerCount'},传入一个对象
// 你定义什么名字,就绑定什么名字到实例上
// 实测无法绑定返回函数的getter
...Vuex.mapGetters(['pCount'])
},
methods: {
add() {
// 最多只能接收一个payload
// 所以大多数情况下payload为对象
store.commit('add', 10, 10);
},
minus() {
store.commit('minus')
}
},
}).$mount("#app");
</script>
</body>
mutation需要遵守Vue的响应规则
- 最好提前在你的 store 中初始化好所有所需属性。
- 当需要在对象上添加新属性时,你应该
使用
Vue.set(obj, 'newProp', 123)
, 或者-
以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
-
使用常亮替代mutation类型
首先在对应的目录新建一个mutation-types的js文件,用于导出所有mutation函数名
然后在index.html中就可以这样写
// 使用es6语法导入mutation-types
import { ADD, MINUS } from "../lib/mutation-types.js";
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
[ADD]: (state, num, num2) => {
// 10 undefined
console.log(num, num2);
num = num || 1;
return state.count += num;
},
[MINUS]: (state) => { state.count-- }
},
getters: {
powerCount: (state) => (num) => {
console.log(state.count, num);
return state.count ** num;
},
pCount(state) {
return state.count ** state.count;
}
}
})
我这样写在浏览器中会出现错误,也不知道什么情况(猜测大概是因为import需要在一个单独的js文件中写吧)mutation必须是同步函数,不支持异步操作
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
store.commit({
type: 'increment',
amount: 10
})
你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add">+</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state) => {
return state.count ++;
},
minus: (state) => { state.count-- }
}
})
new Vue({
// 必须绑定store,不然mapState报一些奇怪的错误
store,
computed: {
...Vuex.mapState(['count'])
},
methods: {
...Vuex.mapMutations([
// 将add -> mutation映射到this.add
'add','minus'
])
},
}).$mount("#app");
</script>
</body>
不仅仅可以直接映射到方法,甚至还可以映射一个带参数方法
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add(10)">+</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state,num) => {
return state.count += num;
},
minus: (state) => { state.count-- }
}
})
new Vue({
// 必须绑定store,不然mapState报一些奇怪的错误
store,
computed: {
...Vuex.mapState(['count'])
},
methods: {
...Vuex.mapMutations([
// 将add -> mutation映射到this.add
'add','minus'
])
},
}).$mount("#app");
</script>
</body>
跟其他两个map函数一样,同样可以通过一个独享设置别名
// this.madd = this.$store.commit('add')
mapMutations({madd:'add'})
其实mapMutations的映射原理,就是通过对应的实例绑定store选项,然后将对应的函数映射到$store的commit()方法上了
Action
action就是专门用来解决无法异步更新state的,但是大家要明白其实action最后依然调用的是mutation并不是直接更新state的,Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用
context.commit
提交一个 mutation,或者通过context.state
和context.getters
来获取 state 和 getters
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add(10)">+</button>
<button @click="minus">-</button>
<button @click="addAsync(5)">async-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state,num) => {
return state.count += num;
},
minus: (state) => { state.count-- }
},
actions:{
addAsync(context){
setTimeout(()=>{
context.commit('add',10);
},1000)
}
}
})
new Vue({
// 必须绑定store,不然mapState报一些奇怪的错误
store,
computed: {
...Vuex.mapState(['count'])
},
methods: {
...Vuex.mapMutations([
// 将add -> mutation映射到this.add
'add','minus'
]),
addAsync(num){
store.dispatch("addAsync",num);
}
},
}).$mount("#app");
</script>
</body>
action必须使用store的dispatch触发(太像redux了)
Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在组件中分发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小例子
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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 的状态
模块的局部状态
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}