1. 核心概念
- state
- getters
- mutations
- actions
2. state
2.1 单一状态树
Vuex
使用单一状态树——用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。
这也意味着,每个应用将仅仅包含一个 store
实例。
单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
单状态树和模块化并不冲突——后面我们会讨论如何将状态和状态变更事件分布到各个子模块中。
存储在 Vuex
中的数据和 Vue
实例中的 data
遵循相同的规则,例如状态对象必须是纯粹 (plain) 的。参考:Vue#data。
2.2 在 Vue 组件中获得 Vuex 状态
那么我们如何在 Vue
组件中展示状态呢?
由于 Vuex
的状态存储是响应式的,从store
实例中读取状态最简单的方法就是在计算属性中返回某个状态(上一篇的案例中已演示):
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
每当 store.state.count
变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM
。
然而,这种模式导致组件依赖全局状态单例。
在模块化的构建系统中,在每个需要使用state
的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex
通过 store
选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)
):
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
通过在根实例中注册 store
选项,该store
实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store
访问到。让我们更新下 Counter
的实现:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
2.3 state 的创建
存储应用状态数据的对象,state
的值可以是一个对象,也可以是一个返回对象的函数,类似 vue 中组件的 data
,使用函数的方式返回对象可以避免对象引用导致的副作用
问题(与Vue的data属性必须返回一个函数的原因一致,请参考)
// let state = {
// a: 1
// }
let state = _=>({a:1})
const store = new Vuex.Store({
state
})
const store2 = new Vuex.Store({
state
})
console.log(store.state == store2.state)
store.state.a = 100;
console.log(store.state.a, store2.state.a);
- 通过
store.state
访问状态数据 state
数据与组件data
一样是被追踪的
2.4 在组件中使用 store
// stores/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
Vue.use(Vuex)
const store = new Vuex.Store({
state
})
export default store;
// stores/state.js
export default () => ({
title: 'CSDN',
content: 'Web前端工程师'
})
<template>
<div class="home">
<h2>{{title}}</h2>
<div>{{content}}</div>
</div>
</template>
<script>
import store from '@/stores'
export default {
name: 'home',
data() {
return {
title: store.state.title,
content: store.state.content
}
}
}
</script>
问题:
state
的更新并不会更新视图
解决
使用 computed
<template>
<div class="home">
<h2>{{title}}</h2>
<div>{{content}}</div>
</div>
</template>
<script>
import store from '@/stores'
export default {
name: 'home',
computed: {
title() {
return store.state.title
},
content() {
return store.state.content
}
}
}
</script>
2.5 store 配置
如果每个组件在使用 store
的时候都 import
会比较繁琐,这个时候,我们通过 vuex 提供的 store
选项把 store
对象注入到 vue 的原型上
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from '@/stores'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
配置注入后,我们就可以在组件实例中使用 this.$store
来访问 store
对象了
<template>
<div class="home">
<h2>{{title}}</h2>
<div>{{content}}</div>
</div>
</template>
<script>
// import store from '@/stores' // 可以去掉了
export default {
name: 'home',
computed: {
title() {
return this.$store.state.title
},
content() {
return this.$store.state.content
}
}
}
</script>
我们从仓库当中拉取数据与当前组件当中的计算属性一一对应,之前的案例是常规写法,在vue
当中,也提供了一些简化式的写法,即我们马上学习的辅助函数。 =>
理解为帮助我们做一些事情的函数,因此称为辅助函数。
下面我们学习的辅助函数 mapState
,会帮助我们生成诸如计算属性与仓库数据一一对应的关系。
2.6 使用辅助函数 mapState
当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性,让你少按几次键,通常我们把 store
的 state
通过 mapState
函数映射到组件的 computed
上
<template>
<div class="home">
<h2>{{title}}</h2>
<div>{{content}}</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'home',
computed: mapState([
'title','content'
])
}
</script>
通过对象方式进行映射
场景:当组件中已有与
store
同名的数据名称
<template>
<div class="home">
<h1>{{title}}</h1>
<h2>{{subTitle}}</h2>
<div>{{content}}</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'home',
data() {
return {title: 'Vuex'}
},
computed: mapState({
subTitle: 'title',
content: ({content}) => content.length <= 12 ? content : content.substring(0, 12) + '......'
})
}
</script>
2.6.1 官网实例与注意事项
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
2.6.2 example01
2.6.2.1 example01-1
在home
组件中注释掉之前计算属性的代码,调用mapState
函数 =>
传数组进去,把items
、n
传进去
computed: mapState(['items', 'n']),
<=>
效果与之前一样,但是写法更简便 =>
它实际就是帮助我们生成 =>
computed: {
items() {
return this.$store.state.items;
},
n() {
return this.$store.state.n;
}
},
\app\src\views\Home.vue
<template>
<div>
<h2>商品列表 - {{n}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
}
},
filters: {
RMB
},
// computed: {
// items() {
// return this.$store.state.items;
// },
// n() {
// return this.$store.state.n;
// }
// },
computed: mapState(['items', 'n']),
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
效果是一样可以实现的。
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.09
Branch: branch02commit description:a0.09(example01-1—将计算属性的数据以数组传入mapState)
tag:a0.09
2.6.2.1.1 mapState原理
mapState
函数就是用来生成上面的 computed
中的结构
=>
我们大致写一下原理
=>
参数是一个数组
,首先生成一个对象
,然后会帮助我们循环数组,对象里是包含一个个函数,函数的名称就是对象的key
,函数里返回的是仓库下state
底下的数据对象与这个对象的key
一一对应,最终生成此对象结构
。
function myMapState(arr) {
let fns = {};
for (let v of arr) {
fns[v] = function() {
return this.$store.state[v];
}
}
}
这个函数有些时候,如computed
中的名字与$store.state
下的名字同名的时候还好,但是如果不同名,假设data
中有一个n
属性 => computed: mapState(['items', 'n']),
<template>
<div>
<h2>商品列表 - {{n}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n:''
}
},
filters: {
RMB
},
computed: mapState(['items', 'n']),
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.10
Branch: branch02commit description:a0.10(将计算属性的数据以数组传入mapState与data重名会报错)
tag:a0.10
2.6.2.2 example01-2
为了解决这个问题,可以mapState
定义成kay:value
的对象形式
items
不需要起别名的话,名字不用动 => key
与value
一样即可
n
值换一下 => key: stateN
computed: mapState({
stateN: 'n',
}),
=>
<template>
<div>
<h2>商品列表 - {{n}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200
}
},
filters: {
RMB
},
computed: mapState({
items: 'items',
stateN: 'n',
}),
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.11
Branch: branch02commit description:a0.11(example01-2—解决mapState同名情况,传kay => value形式的对象)
tag:a0.11
其实mapState
定义成kay:value
的对象形式,就是为起别名的,当希望mapState
中数据的名称与组件当中的某个计算属性的名称不一致的时候,可采用该方式。
2.6.2.3 example01-3
除了这种方式外,还有另外一种方式 =>
函数式,即回调函数
,它会有一个参数,这个参数其实就是接收到的所有state
。
假设我们要给仓库中的n
做一个处理,在进入组件后都乘10
=>
computed: mapState({
items: 'items',
stateN(state) {
return state.n * 10;
}
}),
=>
<template>
<div>
<h2>商品列表 - {{n}} -{{stateN}}- {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200
}
},
filters: {
RMB
},
computed: mapState({
items: 'items',
stateN(state) {
return state.n * 10;
}
}),
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.12
Branch: branch02commit description:a0.12(example01-3—解决mapState同名情况,用回调函数解决)
tag:a0.12
2.6.2.4 example01-4
还有一种情况,假设组件当中本来就有计算属性,但是要从mapState
也取出计算属性,这个时候希望mapState
生成的计算属性与当前原有的计算属性进行合并。比如原先计算属性有个值为val
,mapState
返回值是一个对象,因此需要对对象解构,然后在与computed
合并。
computed:{
val() {
return 'CSDN';
},
...mapState({
items: 'items',
stateN(state) {
return state.n * 10;
}
})
},
=>
<template>
<div>
<h2>商品列表 - {{val}} - {{n}} - {{stateN}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200
}
},
filters: {
RMB
},
computed:{
val() {
return 'CSDN';
},
...mapState({
items: 'items',
stateN(state) {
return state.n * 10;
}
})
},
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.13
Branch: branch02commit description:a0.13(example01-4—mapState与computed同时存在可合并)
tag:a0.13
2.7 使用扩展运算符组合
mapState
函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?
通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed
属性。
但是自从有了对象展开运算符,我们可以极大地简化写法:
通过对象扩展运算符,可以把 mapState
返回的 state
属性与组件已有计算属性进行融合
<script>
import {mapState} from 'vuex'
export default {
computed: {
computed() {/.../},
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
}
</script>
2.8 组件仍然保有局部状态
使用 Vuex
并不意味着你需要将所有的状态放入Vuex
。虽然将所有的状态放到 Vuex
会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
3. getters
有时候我们需要从 store 中的 state 中派生出一些状态,类似组件的 data
与 computed
,store
也提供了一个 getters
对象来处理派生数据
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex
允许我们在 store
中定义**“getter”(**可以认为是 store
的计算属性)。就像计算属性一样,getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter
接受 state
作为其第一个参数:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
3.1 getters 函数
与组件属性一样,我们是通过定义一个函数的形式来返回派生数据的,getters
函数接受两个参数
- 第一个参数:
state
对象 - 第二个参数:
getters
对象
3.2 通过属性访问
同样的,与组件计算属性一样,默认是通过属性的方式去访问 getters
中的数据的,这种方式与组件的计算属性一样,也是会缓存结果的。
Getter
会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter
也可以接受其他 getter
作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
注意,gette
r 在通过属性访问时是作为 Vue
的响应式系统的一部分缓存其中的。
3.3 通过方法访问
我们还可以通过闭包函数的形式返回一个函数,来实现给 getters
函数传参,需要注意的是这种方式不支持结果缓存。
你也可以通过让getter
返回一个函数,来实现给 getter
传参。在你对 store
里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
3.4 使用辅助函数 mapGetters
与 mapState
函数类似,通常映射到组件的 computed
上。
mapGetters
辅助函数仅仅是将 store
中的getter
映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个getter
属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
3.5 example02
我就不演示辅助函数,它比较简单。 =>
实现找出 超过500
的数据
3.5.1 example02-1
app\src\store\index.js
getters: {
// 派生数据
than500(state) {
return state.items.filter( item => item.price > 50000 );
}
},
app\src\views\Home.vue
computed: {
val() {
return 'CSDN';
},
...mapState({
stateN(state) {
return state.n * 10;
}
}),
items() {
console.log(this.$store.getters.than500);
}
},
computed: {
val() {
return 'CSDN';
},
...mapState({
stateN(state) {
return state.n * 10;
}
}),
items() {
return this.$store.getters.than500;
}
},
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.14
Branch: branch02commit description:a0.14(example02-1—实现找出 超过
500
的数据)tag:a0.14
3.5.2 example02-2
有的时候需要传参,我们不定死必须大于500
,而是自定义。
原始数据与派生数据不一样,原始数据该是怎样就怎样,而派生数据是根据条件自动生成,默认情况就和计算属性一样,直接通过以上的方式调用即可,而还有一种形式是根据某种参数或数据去改变这个值。
如在Home
组件上有一个input
框,我们希望实现根据input
框里的值,过滤出满足这个条件的数据。
=> getters
返回一个函数 => this.$store.getters.than500
里存就是一个函数(这其实相当于函数引用或者函数指针)
app\src\store\index.js
getters: {
// 派生数据
than500(state) {
return function(price = 0) {
return state.items.filter( item => item.price > price );
}
}
},
app\src\views\Home.vue
computed: {
val() {
return 'CSDN';
},
...mapState({
stateN(state) {
return state.n * 10;
}
}),
items() {
let rs = this.$store.getters.than500(500000); // 分为单位,实际5000元
console.log(rs);
return rs;
}
},
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.15
Branch: branch02commit description:a0.15(example02-2—实现找出 超过自定义数值的数据)
tag:a0.15
4. mutations
更改 Vuex
的store
中的状态的唯一方法是提交 mutation
(类似 redux
中的 action + reducer
),Vuex
中的mutation
非常类似于事件:每个 mutation
都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)
mutation
中的函数不要直接调用
这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state
作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你不能直接调用一个 mutation handler
。这个选项更像是事件注册:“当触发一个类型为 increment
的 mutation
时,调用此函数。”要唤醒一个 mutation handler
,你需要以相应的 type
调用 store.commit 方法:
store.commit('increment')
4.1 提交(载荷Payload)
store.commit(type, payload)
// or
store.commit({
type: ...,
payload: ...
})
type
要提交的 mutation
回调函数名称
payload
载荷:提交的额外数据,任意格式
你可以向 store.commit
传入额外的参数,即mutation
的 载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation
会更易读:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
4.2 Mutation 需遵守 Vue 的响应规则
既然 Vuex
的store
中的状态是响应式的,那么当我们变更状态时,监视状态的Vue
组件也会自动更新。这也意味着 Vuex
中的 mutation
也需要与使用Vue
一样遵守一些注意事项:
- 最好提前在你的
store
中初始化好所有所需属性。 - 当需要在对象上添加新属性时,你应该
-
使用
Vue.set(obj, 'newProp', 123)
, 或者 -
以新对象替换老对象。例如,利用对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
4.3 使用常量替代 Mutation 事件类型
使用常量替代mutation
事件类型在各种Flux
实现中是很常见的模式。这样可以使 linter
之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个app
包含的mutation
一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
4.4 mutation 函数
mutation
中的函数被 commit
执行的时候,接收两个参数
- 第一个参数:
state
对象 - 第二个参数:
commit
提交的payload
在 mutation
函数中,我们就可以通过 state
对象进行状态数据的修改
4.5 使用辅助函数 mapMutations
mapMutations
函数的使用与 mapState
和 mapGetters
类似,但是其一般是把组件的 methods
映射为 store
的 mutations
的 commit
调用
4.6 mutation 函数必须是同步的
commit
方法没有返回值
一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
现在想象,我们正在 debug
一个 app
并且观察 devtool
中的 mutation
日志。每一条 mutation
被记录,devtools
都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation
中的异步函数中的回调让这不可能完成:因为当 mutation
触发的时候,回调函数还没有被调用,devtools
不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
4.7 在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation
,或者使用 mapMutations
辅助函数将组件中的 methods
映射为 store.commit
调用(需要在根节点注入 store
)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
在 mutation
中混合异步调用会导致你的程序很难调试。
例如,当你调用了两个包含异步回调的 mutation
来改变状态,你怎么知道什么时候回调和哪个先回调呢?
这就是为什么我们要区分这两个概念。在 Vuex
中,mutation 都是同步事务:
store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。
为了处理异步操作,让我们来看一看[Action]
。
4.8 实例
4.8.1 example03
4.8.1.1 example03-1
现在我们不再定死数据了,数据从后端获取,向后端发送请求。
\app\src\apis\index.js
import axios from 'axios'
import URLS from './URLS'
// export async function getItems() {
export async function getItems(sort='desc') {
let rs = await axios({
url: URLS.ITEMS + '?sort=' + sort
// url: URLS.ITEMS
});
return rs;
}
export async function getItem(id) {
let rs = await axios({
url: URLS.ITEM + '/' + id
});
return rs;
}
我们在Home
组件的created
周期中,我们可以调用之前写的apis
里发送请求 =>
let rs = await apis.getItems()
原先是这样请求数据后,将其赋值给data
。而现在需要首先存给仓库,因此我们需要在仓库中写一个方法mutation
=> async updateItems(state, payload){state.items = payload;}
\app\src\views\Home.vue
<template>
<div>
<h2>商品列表 - {{val}} - {{n}} - {{stateN}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200
}
},
filters: {
RMB
},
computed:{
val() {
return 'CSDN';
},
...mapState({
stateN(state) {
return state.n * 10;
}
}),
items() {
// let rs = this.$store.getters.than500(500000); // 分为单位,实际5000元
let rs = this.$store.state.items;
console.log(rs);
return rs;
}
},
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
},
async created() {
let rs = await apis.getItems()
this.$store.commit('updateItems', rs.data);
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
\app\src\store\index.js
mutations: {
changeN(state, payload) {
state.n = payload;
},
addItem(state, payload) {
// state.items = [{
// id: ++maxId,
// ...payload
// }, ...state.items];
},
async updateItems(state, payload) {
state.items = payload;
},
}
效果是一样的,这个时候是先发请求获取数据,再向仓库发请求更新仓库 =>
Home
组件中computed
属性中的items
属性也跟着计算更新了。
这里虽然是在组件中请求了数据实际还得走仓库绕一绕。
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.16
Branch: branch02commit description:a0.16(example03-1—从后端请求数据)
tag:a0.16
4.8.1.2 example03-2
但是以上的做法并不值得推荐,这样调用apis
的行为可能不止一个页面去用,要是成千个页面,你怎么整?
不可能每个地方都写吧?就相当于当初我们把axios
请求封装起来一样,不可能仅仅一处用,为了用起来方便,我们才去封装的。
同样也许很多页面都要调用这些数据,然后再更新仓库数据,我们最好把请求也放到仓库中去做,封装起来。
=>
在Home
组件的created
中只需要提交的更新请求动作(可以带一个sort
参数过去)即可,剩下全封装在仓库中。
\app\src\views\Home.vue
<template>
<div>
<h2>商品列表 - {{val}} - {{n}} - {{stateN}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200
}
},
filters: {
RMB
},
computed:{
val() {
return 'CSDN';
},
...mapState({
items:'items',
stateN(state) {
return state.n * 10;
}
})
},
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem', {
"name": val,
"vendor":"Apple",
"price":1949900
})
}
}
},
async created() {
this.$store.commit('updateItems', {sort: 'desc'});
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
\app\src\store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as apis from '@/apis';
Vue.use(Vuex);
// let items = [{"id":3,"name":"Macbook Pro 15.4","vendor":"Apple","price":1949900},{"id":4,"name":"Apple iMac","vendor":"Apple","price":1629900},{"id":9,"name":"游戏本2019款","vendor":"XiaoMi","price":879900},{"id":6,"name":"Apple Watch Series 4","vendor":"Apple","price":599900},{"id":1,"name":"iPhone XR","vendor":"Apple","price":542500},{"id":11,"name":"HUAWEI P30 Pro","vendor":"HuaWei","price":498800},{"id":2,"name":"Apple iPad Air 3","vendor":"Apple","price":377700},{"id":10,"name":"HUAWEI P30","vendor":"HuaWei","price":368800},{"id":7,"name":"小米9","vendor":"XiaoMi","price":259900},{"id":12,"name":"华为平板 M6 10.8英寸","vendor":"HuaWei","price":229900},{"id":16,"name":"Redmi K20","vendor":"XiaoMi","price":199900},{"id":13,"name":"HUAWEI WATCH GT","vendor":"HuaWei","price":128800},{"id":5,"name":"Apple Magic Mouse","vendor":"Apple","price":72900},{"id":8,"name":"小米手环4","vendor":"XiaoMi","price":16900}];
//
// let maxId = items.reduce(function(maxId, item ) {
// return item.id > maxId ? item.id : maxId;
// }, 0);
let items = [];
let store = new Vuex.Store({
state: {
n: 10,
items
},
mutations: {
changeN(state, payload) {
state.n = payload;
},
addItem(state, payload) {
},
async updateItems(state, payload) {
let rs = await apis.getItems(payload);
state.items = rs.data;
},
},
getters: {
// 派生数据
than500(state) {
return function(price = 0) {
return state.items.filter( item => item.price > price );
}
}
}
});
export default store;
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.17
Branch: branch02commit description:a0.17(example03-2—向后端请求数据封装进仓库里)
tag:a0.17
4.8.1.3 example03-3
看起来这样做是没有任何问题的,但是实际上还是有一些问题。
\server\app.js
const Koa = require('koa');
const KoaRouter = require('koa-router');
const KoaBodyParser = require('koa-bodyparser')
let datas = {
items: require('./data/items.json'),
users: require('./data/users.json')
}
const app = new Koa();
let currentId = 20;
app.use( async (ctx, next) => {
await next();
} );
app.use(KoaBodyParser());
const router = new KoaRouter();
router.get('/', async ctx => {
ctx.body = 'api';
});
router.get('/items', async ctx => {
let sort = ctx.request.query.sort || 'desc';
let items = datas.items.sort((a, b) => sort === 'asc' ? a.price - b.price : b.price - a.price);
ctx.body = items;
});
router.get('/item/:id', async ctx => {
let id = Number(ctx.params.id);
let item = datas.items.find(item => item.id === id);
if (!item) {
ctx.throw(404, '没有该商品信息');
return;
}
ctx.body = item;
});
router.post('/add', async ctx => {
let {name} = ctx.request.body;
if (name === '') {
ctx.body = {
code: 1,
message: '商品名称不能为空'
}
return;
}
let newData = {
id: currentId++,
name
};
datas.items.unshift(newData);
ctx.body = {
code: 0,
message: '提交成功',
data: newData
}
})
app.use( router.routes() );
app.listen(7777);
我们在对接后端接口apis文件中写一个提交接口
\app\src\apis\URLS.js
export default {
'ITEMS': '/api/items',
'ITEM': '/api/item',
'ADD_ITEM': '/api/add'
}
\app\src\apis\index.js
import axios from 'axios'
import URLS from './URLS'
// export async function getItems() {
export async function getItems(sort='desc') {
let rs = await axios({
url: URLS.ITEMS + '?sort=' + sort
// url: URLS.ITEMS
});
return rs;
}
export async function getItem(id) {
let rs = await axios({
url: URLS.ITEM + '/' + id
});
return rs;
}
export async function postItem(data) {
let rs = await axios({
method: 'post',
url: URLS.ADD_ITEM,
data
});
return rs;
}
Home
组件中,触发提交只需要向仓库提交一个名称即可。
仓库的mutations
中的addItem
动作中,调用后端apis
的提交接口,然后判断后端请求的返回值是否出错(code
为0
成功),成功之后再更新进仓库。
\app\src\views\Home.vue
methods: {
addItem() {
let val = this.$refs.input.value;
if (val !== '') {
this.$store.commit('addItem',{'name':val});
}
}
}
test01\app\src\store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as apis from '@/apis';
Vue.use(Vuex);
let items = [];
let store = new Vuex.Store({
state: {
n: 10,
items
},
mutations: {
changeN(state, payload) {
state.n = payload;
},
async addItem(state, payload) {
let rs = await apis.postItem(payload);
if (!rs.data.code) {
state.items.unshift(rs.data.data);
}
},
async updateItems(state, payload) {
let rs = await apis.getItems(payload);
state.items = rs.data;
},
},
getters: {
// 派生数据
than500(state) {
return function(price = 0) {
return state.items.filter( item => item.price > price );
}
}
}
});
export default store;
做前后端交互必须检查network
,在network
中检查请求是成功了。主要看Header
中的提交地址有没有问题?提交方法、状态码、头信息、以及带过去的数据是否没问题。再看Response
返回值,是否正确。紧接着再刷新页面,数据也是存在着的。
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.18
Branch: branch02commit description:a0.18(example03-3—向仓库添加数据并更新到后端)
tag:a0.18
4.8.1.4 example03-4
好像看起来似乎是没有任何问题了,但是实际上提交是有可能会失败的。
后端判断添加数据排除空格 =>
router.post('/add', async ctx => {
let {name} = ctx.request.body;
if (name.trim() === '') {
ctx.body = {
code: 1,
message: '商品名称不能为空'
}
return;
}
let newData = {
id: currentId++,
name
};
datas.items.unshift(newData);
ctx.body = {
code: 0,
message: '提交成功',
data: newData
}
})
我们现在输入一些空格
添加数据,后端肯定返回失败。 => 实现前端显示错误提示
首先点击提交按钮,触发仓库addItem
请求动作,由仓库的mutations
中addItem
执行,并向后端发送添加数据请求,如果提交成功,则更新仓库数据,否则返回后端response
的数据。
即在Home
组件的addItem
中获取提交数据的结果,因此我们需要addItem
上添加async
和await
。
可能有人问为啥不直接在仓库下index
中处理错误结果呢,还要返回给Home
组件呢?
希望我们操作完数据并希望能在界面上有所体现的,所以界面组件是需要返回值做判断的。同时仓库中是不能操作组件的,会不太方便的,并且这样增加了组件间耦合度,不利于代码复用的。
其实将UI
的逻辑和数据逻辑全部混合在一起,是不推荐的,数据处理数据的,要是需要界面产生交互,应该由数据去通知UI
界面,这样的设计更为理想一些。
\app\src\store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as apis from '@/apis';
Vue.use(Vuex);
// let items = [{"id":3,"name":"Macbook Pro 15.4","vendor":"Apple","price":1949900},{"id":4,"name":"Apple iMac","vendor":"Apple","price":1629900},{"id":9,"name":"游戏本2019款","vendor":"XiaoMi","price":879900},{"id":6,"name":"Apple Watch Series 4","vendor":"Apple","price":599900},{"id":1,"name":"iPhone XR","vendor":"Apple","price":542500},{"id":11,"name":"HUAWEI P30 Pro","vendor":"HuaWei","price":498800},{"id":2,"name":"Apple iPad Air 3","vendor":"Apple","price":377700},{"id":10,"name":"HUAWEI P30","vendor":"HuaWei","price":368800},{"id":7,"name":"小米9","vendor":"XiaoMi","price":259900},{"id":12,"name":"华为平板 M6 10.8英寸","vendor":"HuaWei","price":229900},{"id":16,"name":"Redmi K20","vendor":"XiaoMi","price":199900},{"id":13,"name":"HUAWEI WATCH GT","vendor":"HuaWei","price":128800},{"id":5,"name":"Apple Magic Mouse","vendor":"Apple","price":72900},{"id":8,"name":"小米手环4","vendor":"XiaoMi","price":16900}];
//
// let maxId = items.reduce(function(maxId, item ) {
// return item.id > maxId ? item.id : maxId;
// }, 0);
let items = [];
let store = new Vuex.Store({
state: {
n: 10,
items
},
mutations: {
changeN(state, payload) {
state.n = payload;
},
async addItem(state, payload) {
let rs = await apis.postItem(payload);
if (!rs.data.code) {
state.items.unshift(rs.data.data);
}
},
async updateItems(state, payload) {
let rs = await apis.getItems(payload);
state.items = rs.data;
},
},
getters: {
// 派生数据
than500(state) {
return function(price = 0) {
return state.items.filter( item => item.price > price );
}
}
}
});
export default store;
\app\src\views\Home.vue
methods: {
async addItem() {
let val = this.$refs.input.value;
if (val !== '') {
let rs = await this.$store.commit('addItem',{'name':val});
console.log(rs);
}
}
}
打印的结果是undefined
,说明Home组件中的this.$store.commit('addItem',{'name':val})
并不会返回任何东西。 =>
这是因为store
中的mutations
中的函数不对异步代码进行处理。
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.19
Branch: branch02commit description:a0.19(example03-4—store中的mutations中的函数不对异步代码进行处理)
tag:a0.19
4.8.1.5 小结
在commit
的时候,其实就是在给commit
函数传递一个函数名,它会在mutations
中寻找这个函数名,并把它执行了。实际函数的执行,是由commit
内部方法决定的,并且commit
这个方法没有加async
,即commit
函数并没有
=> return await mutations[fnName]();
=> 即commit
方法只执行这些函数,并不会返回任何值,可以理解为它除了帮忙调用函数外,其他任何事都不做。 => commit
不会对异步任务做任何处理,即我们刚写的异步任务在commit
中就不会等待,因此我们在Home
组件中是获取不到commit
的返回值的。
function commit(fnName) {
mutations[fnName]();
}
实际Mutations
是允许包含异步任务的,如果只做处理,不做任何返回的话,其实大部分情况下是没有任何问题的
=> 这个时候就需要actins
来解决问题了
5. actions
action
中的函数与 mutation
中的函数类似,但是它主要用来进行异步任务的处理,然后通过提交 mutation
来修改 state
注意:
action
中的函数不要直接修改state
不同(mutation
)在于:
Action
提交的是mutation
,而不是直接变更状态。Action
可以包含任意异步操作。
让我们来注册一个简单的 action
:
const store = new Vuex.Store({
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')
}
}
5.1 提交
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 产生的副作用(即状态变更)。
store.dispatch(type, payload)
// or
store.dispatch({
type: ...,
payload: ...
})
action
任务需要通过 dispatch
方法来提交(派发),与 commit
类似
dispatch
方法有返回值,且一定返回一个 promise
对象
5.1.1 在组件中提交
你在组件中使用 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')`
})
}
}
5.2 组合 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 才会执行。
5.3 action 函数
action
中的函数执行的过程中也接受两个参数
- 第一个参数:
store
对象 - 第二个参数:
dispatch
提交的payload
5.4 使用辅助函数 mapActions
与 mapMutations
函数类似,把组件的 methods
映射为 store
的 actions
的 dispatch
调用
5.5 实例
5.5.1 example04
这里不知你直接调用state
了,而是需要调用commit
,请求mutation
中的方法。 => store.commit('addItem', rs.data.data);
5.5.1.1 利用action发请求的整个流程
ui => dispatch => actions => commit => mutations => state
5.5.1.2 example04-1
\app\src\store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as apis from '@/apis';
Vue.use(Vuex);
let items = [];
let store = new Vuex.Store({
state: {
n: 10,
items
},
getters: {
// 派生数据
than500(state) {
return function(price = 0) {
return state.items.filter( item => item.price > price );
}
}
},
mutations: {
changeN(state, payload) {
state.n = payload;
},
// mutations 中的函数不对异步代码进行处理
addItem(state, payload) {
state.items.unshift(payload);
},
async updateItems(state, payload) {
let rs = await apis.getItems(payload);
state.items = rs.data;
},
},
actions: {
async addItem(store, payload) {
let rs = await apis.postItem(payload);
if (!rs.data.code) {
// action 里面不能直接处理state
// state.items.unshift(rs.data.data);
store.commit('addItem', rs.data.data);
}
return rs;
}
}
});
export default store;
\app\src\views\Home.vue
<template>
<div>
<h2>商品列表 - {{n}} - {{stateN}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<span v-show="message">{{message}}</span>
<hr>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200,
message: ''
}
},
filters: {
RMB
},
computed:{
...mapState({
items:'items',
stateN(state) {
return state.n * 10;
}
})
},
methods: {
async addItem() {
let val = this.$refs.input.value;
if (val !== '') {
// 提交是有可能会失败的
let rs = await this.$store.dispatch('addItem', {
"name": val
});
// console.log(rs);
if (rs.data.code) {
this.message = rs.data.message;
} else {
this.message = '提交成功';
}
// this.message = '添加失败';
}
}
},
async created() {
this.$store.commit('updateItems', {sort: 'desc'});
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.20
Branch: branch02commit description:a0.20(example04-1—利用action解决异步返回问题)
tag:a0.20
5.5.1.2.1 小结
这样就把数据的处理
和UI处理
逻辑分离开了,更容易复用功能,如另外一个页面不需要这样的提示,都封装在仓库的addItem
中处理,就难以复用了。所以数据就单独只有数据的处理,不要再做其他工作。
注意store
中mutations
并不是处理不了异步任务,而是不对异步任务的返回进行处理。但其实就算同步任务也是不行的,就理解mutations
只负责做事,不返回结果。如希望我们的操作反馈结果的话,这个时候就需要action
了。实际同步任务返不返回结果都无所谓,因为我们直接可以在仓库的state
中获取了。但是如果异步任务我们也通过此种方式获取,就不是最新的值,因为会存在延迟。
5.5.1.3 example04-2
我们演示一下,在Home
加一个按钮 => <button @*click*="getData">按钮</button>
仓库中changeN
不涉及异步处理,同步修改 =>
this.$store.commit(‘changeN’, 11)
获取返回值 => console.log(this.$store.state.n)
<template>
<div>
<h2>商品列表 - {{n}} - {{stateN}} - {{$store.state.n}}</h2>
<input type="text" ref="input" /><button @click="addItem">提交</button>
<span v-show="message">{{message}}</span>
<hr>
<button @click="getData">按钮</button>
<ul class="item-list">
<li class="head">
<span>名称</span>
<span>价格</span>
<span>操作</span>
</li>
<li v-for="item of items" :key="item.id">
<span>
<router-link :to="{name: 'view', params: {id: item.id}}">{{item.name}}</router-link>
</span>
<span>{{item.price|RMB}}</span>
<span>
<button>添加到购物车</button>
</span>
</li>
</ul>
</div>
</template>
<script>
import * as apis from '@/apis'
import {RMB} from "@/filters/RMB";
import {mapState} from "vuex";
export default {
name: "Home",
data() {
return {
n: 200,
message: ''
}
},
filters: {
RMB
},
computed:{
...mapState({
items:'items',
stateN(state) {
return state.n * 10;
}
})
},
methods: {
async addItem() {
let val = this.$refs.input.value;
if (val !== '') {
// 提交是有可能会失败的
let rs = await this.$store.dispatch('addItem', {
"name": val
});
// console.log(rs);
if (rs.data.code) {
this.message = rs.data.message;
} else {
this.message = '提交成功';
}
// this.message = '添加失败';
}
},
getData() {
this.$store.commit('changeN', 11);
console.log(this.$store.state.n);
}
},
async created() {
this.$store.commit('updateItems', {sort: 'desc'});
}
}
</script>
<style>
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.item-list li {
padding: 10px;
display: flex;
justify-content: space-between;
height: 30px;
line-height: 30px;
border-bottom: 1px dotted #333;
}
.item-list li.head {
font-weight: bold;
}
.item-list li span {
min-width: 200px;
}
</style>
同步虽然在mutations
不能有返回值,但是它会同步修改state
的,我们直接通过state
就可以获取最新的值了。
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.21
Branch: branch02commit description:a0.21(example04-2—同步任务mutations如何获取返回值)
tag:a0.21
5.5.1.4 example04-3
如果这个修改是异步的就有问题了
app\src\store\index.js
changeN(state, payload) {
// state.n = payload;
setTimeout(() => {
state.n = payload;
}, 1000);
},
因为是异步的会有延迟,因此我们第一次取得值不是最新的。
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.22
Branch: branch02commit description:a0.22(example04-3—异步任务mutations获取返回值出现延迟)
tag:a0.22
5.5.1.5 example04-4
但是如果把它放在actions中 =>
Home.vue
export default {
name: "Home",
data() {
return {
n: 200,
message: ''
}
},
filters: {
RMB
},
computed:{
...mapState({
items:'items',
stateN(state) {
return state.n * 10;
}
})
},
methods: {
async addItem() {
let val = this.$refs.input.value;
if (val !== '') {
// 提交是有可能会失败的
let rs = await this.$store.dispatch('addItem', {
"name": val
});
// console.log(rs);
if (rs.data.code) {
this.message = rs.data.message;
} else {
this.message = '提交成功';
}
// this.message = '添加失败';
}
},
async getData() {
let rs = await this.$store.dispatch('changeAsyncN', 11);
console.log(rs);
console.log(this.$store.state.n);
}
},
async created() {
this.$store.commit('updateItems', {sort: 'desc'});
}
}
app\src\store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as apis from '@/apis';
Vue.use(Vuex);
let items = [];
let store = new Vuex.Store({
state: {
n: 10,
items
},
getters: {
// 派生数据
than500(state) {
return function(price = 0) {
return state.items.filter( item => item.price > price );
}
}
},
mutations: {
changeN(state, payload) {
state.n = payload;
// setTimeout(() => {
// state.n = payload;
// }, 1000);
},
// mutations 中的函数不对异步代码进行处理
addItem(state, payload) {
state.items.unshift(payload);
},
async updateItems(state, payload) {
let rs = await apis.getItems(payload);
state.items = rs.data;
},
},
actions: {
async addItem(store, payload) {
let rs = await apis.postItem(payload);
if (!rs.data.code) {
// action 里面不能直接处理state
// state.items.unshift(rs.data.data);
store.commit('addItem', rs.data.data);
}
return rs;
},
async changeAsyncN(store, payload) {
return new Promise( (resolve) => {
setTimeout(() => {
store.commit('changeN', payload);
resolve('changeAsyncN'+payload);
}, 1000);
} );
}
}
});
export default store;
参考:https://github.com/6xiaoDi/blog-Vuex-Novice/tree/a0.23
Branch: branch02commit description:a0.23(example04-4—解决异步任务mutations获取返回值出现延迟)
tag:a0.23
6. Module
这个更多的是基于一种代码组织结构上的辅助。
模块主要用来组织mutations
和getter
、setter
、action
的方法,当项目很庞大的时候,可能仓库的数据特别繁琐,为了能让仓库的数据组织有序,看起来结构清晰,这个时候就有了模块的概念,它就相当于命名空间的概念,用模块来相互独立。
注意如果有些数据在模块外,不是直接获取的,而是rootState
获取。
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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 的状态
6.1 模块的局部状态
对于模块内部的mutation
和 getter
,接收的第一个参数是模块的局部状态对象。
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
同样,对于模块内部的 action
,局部状态通过 context.state
暴露出来,根节点状态则为 context.rootState
:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
对于模块内部的getter
,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
6.2 命名空间
默认情况下,模块内部的 action
、mutation
和 getter
是注册在全局命名空间的——这样使得多个模块能够对同一 mutation
或 action
作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter
、action
及mutation
都会自动根据模块注册的路径调整命名。例如:
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
启用了命名空间的getter
和 action
会收到局部化的 getter
,dispatch
和 commit
。换言之,你在使用模块内容(module assets
)时不需要在同一模块内额外添加空间名前缀。更改 namespaced
属性后不需要修改模块内的代码。
6.2.1 在带命名空间的模块内访问全局内容(Global Assets)
如果你希望使用全局 state
和 getter
,rootState
和 rootGetters
会作为第三和第四参数传入 getter
,也会通过 context
对象的属性传入 action
。
若需要在全局命名空间内分发 action
或提交mutation
,将 { root: true }
作为第三参数传给 dispatch
或 commit
即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
6.2.2 在带命名空间的模块注册全局 action
若需要在带命名空间的模块注册全局action
,你可添加 root: true
,并将这个 action
的定义放在函数 handler
中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
6.2.3 带命名空间的绑定函数
当使用 mapState
, mapGetters
, mapActions
和 mapMutations
这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
而且,你可以通过使用 createNamespacedHelpers
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
6.2.4 给插件开发者的注意事项
如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store
,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:
// 通过插件的参数对象得到空间名称
// 然后返回 Vuex 插件函数
export function createPlugin (options = {}) {
return function (store) {
// 把空间名字添加到插件模块的类型(type)中去
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
6.3 模块动态注册
在 store
创建之后,你可以使用 store.registerModule
方法注册模块:
import Vuex from 'vuex'
const store = new Vuex.Store({ /* 选项 */ })
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
之后就可以通过 store.state.myModule
和 store.state.nested.myModule
访问模块的状态。
模块动态注册功能使得其他Vue
插件可以通过在 store
中附加新模块的方式来使用 Vuex
管理状态。例如,vuex-router-sync
插件就是通过动态注册模块将vue-router
和 vuex
结合在一起,实现应用的路由状态管理。
你也可以使用 store.unregisterModule(moduleName)
来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建store
时声明的模块)。
注意,你可以通过 store.hasModule(moduleName)
方法检查该模块是否已经被注册到store
。
6.3.1 保留 state
在注册一个新 module
时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state
。你可以通过 preserveState
选项将其归档:store.registerModule('a', module, { preserveState: true })
。
当你设置 preserveState: true
时,该模块会被注册,action
、mutation
和 getter
会被添加到 store
中,但是state
不会。这里假设store
的 state
已经包含了这个 module
的 state
并且你不希望将其覆写。
6.4 模块重用
有时我们可能需要创建一个模块的多个实例,例如:
- 创建多个 store,他们公用同一个模块 (例如当
runInNewContext
选项是false
或'once'
时,为了在服务端渲染中避免有状态的单例) - 在一个 store 中多次注册同一个模块
如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。
实际上这和 Vue 组件内的 data
是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):
const MyReusableModule = {
state: () => ({
foo: 'bar'
}),
// mutation, action 和 getter 等等...
}
7. 重点说明
其实对于中小应用而言,仓库管理不常用,因为如何判断这个数据是否常用呢?
它有两个参考点 =>
多组件共享(且共享麻烦的情况下) =>
如果数据在多个组件共享,同时很麻烦,如同级组件或嵌套过多的父子组件的子孙节点,通过组件的
props
传递数据麻烦的情况下。
多页面共享 =>
路由跳转的过程中也需要共享数据
如当前的Home
组件,获取这些数据存在仓库中,如果有分页功能,点击每页都需要向后端请求数据,然后又把它往仓库中存储,这样是否合理呢?
本质上没啥问题,但是实际上产生了另外的问题 =>
数据的缓存性
如我们跳回第一页,之前已经获取过这个数据了,现在实际不需要再向后端请求了,那你可能想到直接去仓库中获取那一页的数据就好了,这样可以节省请求。但是实际上这会造成另一个问题,它的时效性问题,除非这个数据变更不太大的情况下可以这么去做,实际这里就是把仓库作为了缓存容器使用了。
=>
先判断你要的数据存不存在,如果存在就直接去仓库中取,如果不存在就去后端拉取过来,再更新仓库即可,最后前端页面再去使用即可。这样实际上,带来很大的问题就是数据的维护性极差,这个时候去处理缓存的问题及缓存命中、缓存时效性的问题。
一般情况下,看应用的需求再来决定用不用仓库,一般它存储当前用户登录状态,这个时候做页面和路由跳转的时候就会很方便了。还有一些比较常用和通用的数据也会常常存在仓库中,像比较独立性的数据。
如做商城的时候,商品的量实际非常大,通过仓库去维护这么大的数据实际并不合适,但可以优化,比如只存前5
页的数据,而后5
页的数据直接请求就好了(一般用户都会浏览前几页,很少有人喜欢倒着浏览,甚至超过5页浏览的人也不太多),因此可以把前5
页缓存到仓库中。可以在5分钟
内在仓库中取,否则的话从后端去取然后更新仓库。
一定注意不要什么数据都扔到仓库中,这样维护起来的成本很大很麻烦。
考虑到在blog中不好体现代码更改的位置,小迪才用github托管代码,大家可以查看github,看到详细版本修改过程,搭配博客学习。
(后续待补充)