一. 概述
1 Vuex是什么
Vuex是一个集中式的状态管理插件, 实现所有组件的状态(数据)共享, 也是组件间通信的一种方式
- Vuex本质上是一个Vue的插件
- 主要实现所有组件的状态共享
2 什么时候用Vuex
一句话总结
官方推荐在中大型单页应用中使用
如何界定呢? 例如…….
- 需要与第三方的系统深度整合, 涉及异步获取数据, 大量使用全局变量, 业务逻辑比较复杂等情况时
- 如果超过3个组件需要
引用/修改
同一个数据结构, 比如token, 用户收藏, 购物车中的商品 - 涉及到深层嵌套的组件需要与其它组件进行数据通讯
二. Vuex的工作原理
1 设计思路
Vuex可以理解成一个统一管理数据的仓库, 因此通常使用store
表示
理论上, 所有的组件都可以读/写
store中的数据
怎样实现读数据
可以定义一个单独的全局变量, 所有的组件从全局对象中获取数据
怎样实现写数据
如果在每个组件中各自修改全局变量, 就会引起混乱, 因此设计一个工具集中的管理写操作
如何设计集中式的写操作
所有的组件不能自己修改全局变量, 需要通知Vuex, 由Vuex集中管理, 统一修改
OK, 这样Vuex里至少应该包含两个部分
- state: 保存全局数据
- actions: 对数据进行修改的动作
因此, 对于简单的状态管理, 我们使用类似于下面的store模式就可以
Vuex在设计时, 将动作
拆分成了两个部分
- Actions: 处理业务逻辑和异步任务
- Mutations: 对状态的修改
2 核心概念
1) Actions
动作, 行为
对象类型, 处理逻辑和异步操作
2) Mutations
变化, 转变
对象类型, 定义方法操作数据(同步)
3) State
状态, 数据
对象类型, 保存数据
三. 起步
1 创建vue项目
使用vue脚本创建项目vuex-demo
vue create vuex-demo
编写一个基本的计数器
组件
<template>
<div>
<h1>点击了{{ count }}次</h1>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="incrementIfOdd">是奇数就+1</button>
<button @click="incrementAsync">异步+1</button>
</div>
</template>
<script>
export default {
name: 'Counter',
data() {
return {
count: 0,
}
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementIfOdd() {
if (this.count % 2) this.count++
},
incrementAsync() {
setTimeout(() => {
this.count++
}, 500)
},
},
}
</script>
挂载到App.vue下
<template>
<div id="app">
<counter></counter>
</div>
</template>
<script>
import Counter from '@/components/Counter.vue'
export default {
name: 'App',
components: {
Counter,
},
}
</script>
2 安装Vuex插件
使用npm安装Vuex
npm i vuex@3
:::tips 特别说明
一般在src
目录下创建store/index.js
// 导入Vue
import Vue from 'vue'
// 导入vuex
import Vuex from 'vuex'
// vuex本质上是vue的一个插件, 通过Vue.use()注册
// 上所有的vue实例上挂载$store
Vue.use(Vuex)
// state: 状态
const state = {}
// mutations: 修改状态
const mutations = {}
// actions: 动作
const actions = {}
// 创建并导出store对象
export default new Vuex.Store({
state,
mutations,
actions,
})
Store包含三个部分
- state: 对象类型, 保存数据
- mutations: 对象类型, 定义方法操作数据
- actions: 对象类型, 处理逻辑和异步操作
4 导入
在main.js
中, 完成store
对象的导入
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store, // store: store
render: (h) => h(App),
}).$mount('#app')
演示
四. 基本使用
为了熟悉Vuex的使用流程, 先了解单组件与Vuex如何交互
通过单组件熟悉语法和流程, 多组件使用也是类似的
1 完整流程
组件到Vuex的流程, 主要是写
操作, 通过向Vuex发送通知, 让Vuex修改state中的数据
1) 流程图
关键API
- dispatch(分发, 派发): 从组件分发到Actions对象的一个方法
- commit(提交): 提交一个Mutations对象的一个方法
2) 从组件到Actions
dispatch的两个参数
- type: 对应action函数的名称
- payload(可选): 携带的数据
increment() {
// 分发一个action, 对应调用名为increment的方法
this.$store.dispatch('increment')
},
发现报错
原因
在actions
对象中, 并没有定义对应的increment
方法
解决方法
在actions
对象中定义increment
方法
在increment
方法中需要调用commit
, 提交一个mutation
, 所以这里设计了一个context
参数保留了store
大部分的方法, 但是出于性能考虑, 不太需要完整的store
对象, 大家可以理解成一个简化版的store
increment(ctx, value) {
console.log(ctx)
},
通过观察, 我们发现这里也是可以拿到state
的
思考
能否直接在actions
方法中修改state
increment(ctx, value) {
console.log(ctx)
ctx.state.count += value
},
理论上是可以的, 但是不推荐这么做, 原因后面细讲
在actions
里, 最核心的工作是commit到Mutations
3) 从Actions到Mutations
increment(ctx, value) {
ctx.commit('INCRMENT', value)
},
小技巧
为了区分actions
和mutations
, 一般将mutations
的名称大写
此时, 发现没有定义相关的mutation
解决
定义对应的mutations
, 在方法中可以拿到state
, 进而对state
进行操作
INCREMENT(state, value) {
console.log(state)
state.count += value
},
练习
完成decrement
流程
完成incrementIfOdd
流程
完成incrementAsync
流程
参考答案
组件的改造
在组件中通过调用dispatch
分发actions
methods: {
increment() {
// 分发一个action, 对应调用名为increment的方法
this.$store.dispatch('increment', this.num)
},
decrement() {
this.$store.dispatch('decrement', this.num)
},
incrementIfOdd() {
this.$store.dispatch('incrementIfOdd', this.num)
},
incrementAsync() {
this.$store.dispatch('incrementAsync', this.num)
},
},
Actions
const actions = {
increment(ctx, value) {
ctx.commit('INCREMENT', value)
},
decrement(ctx, value) {
ctx.commit('DECREMENT', value)
},
incrementIfOdd(ctx, value) {
if (ctx.state.count % 2) ctx.commit('INCREMENTIFODD', value)
},
incrementAsync(ctx, value) {
setTimeout(() => {
ctx.commit('INCREMENTASYNC', value)
}, 500)
},
}
Mutations
const mutations = {
INCREMENT(state, value) {
console.log(state)
state.count += value
},
DECREMENT(state, value) {
state.count -= value
},
INCREMENTIFODD(state, value) {
state.count += value
},
INCREMENTASYNC(state, value) {
state.count += value
},
}
4) 从Vuex到组件
所有的组件对象都可以通过$store
访问到Vuex, 进而可以拿到state
在组件中使用
<h1>计数总和: {{ $store.state.count }}</h1>
2 简化写流程
我们发现向increment
和decrement
这样的操作在actions
没做什么逻辑操作, 直接转发给Mutations
所以, 我们思考是否可以跳过这个流程, 让组件直接commit到Mutations呢?
答案是肯定的, Vuex团队也注意到了这个问题
1) 流程图
2) 从组件直接到Mutations
示例
methods: {
increment() {
// 分发一个action, 对应调用名为increment的方法
// this.$store.dispatch('increment', this.num)
// 直接使用commit提交给Mutations
this.$store.commit('INCREMENT', this.num)
},
decrement() {
// this.$store.dispatch('decrement', this.num)
// 直接使用commit提交给Mutations
this.$store.commit('DECREMENT', this.num)
},
},
3 深入思考
1) 为什么将Actions和Mutations分开
Vuex设计的目的就是为中大型项目服务的
- Actions属于分层设计, 将复杂业务解耦, 承担了类似
中间件
的角色 - Actions处理异步任务, Mutations处理同步任务
- Mutations方便调试与监控, 能更好地与
devtools
集成
案例
在组件中, 分发一个complex
任务到Actions
<button @click="$store.dispatch('complex')">复杂的逻辑</button>
在Actions中, 拆分成多个子任务, 完成解耦
complex(ctx) {
console.log('处理复杂逻辑...')
ctx.dispatch('sub1')
},
sub1(ctx) {
console.log('处理子任务1...')
ctx.dispatch('sub2')
},
sub2(ctx) {
console.log('处理子任务2...')
ctx.commit('INCREMENT', 1)
},
子任务可以设计成异步任务, 从而提高效率, 而真正对数据的修改统一放到Mutations中完成
2) Vuex调试工具
由于所有的数据修改统一在Mutations中完成, 只需要监控Mutations就可以完全控制state的改变, 方便大型项目中的调试, 这也是为什么不推荐在Actions中直接修改state的原因
4 Getters配置
Vuex中的Getters类似于计算属性, 用于对数据的再次加工
需求
统计已完成的todo项目
1) 定义state
const state = {
count: 0,
todos: [
{ id: 1, content: '待办1', done: true },
{ id: 2, content: '待办2', done: false },
{ id: 3, content: '待办3', done: false },
],
}
2) 定义getters
const getters = {
doneTodos: (state) => {
return state.todos.filter((todo) => todo.done)
},
}
3) 配置getters
// 创建并导出store对象
export default new Vuex.Store({
state,
mutations,
actions,
getters,
})
4) 在组件中调用
<h3>已完成的待办</h3>
<ul>
<li v-for="item in $store.getters.doneTodos" :key="item.id">
{{ item.content }}
</li>
</ul>
5) 其它用法
通过方法访问, 返回一个函数
<div>id为2的todo: {{ $store.getters.getTodoById(2) }}</div>
const getters = {
doneTodos: (state) => {
return state.todos.filter((todo) => todo.done)
},
getTodoById: (state) => {
return (id) => {
return state.todos.find((todo) => todo.id == id)
}
},
}
五. map辅助函数
1 为什么需要辅助函数
在组件中访问state状态时, 每次都需要使用
$store.state.xxx
我们希望在使用的时候直接使用xxx
该如何操作呢
可以在组件中定义计算属性
computed: {
count() {
return this.$store.state.count
},
name() {
return this.$store.state.name
},
age() {
return this.$store.state.age
},
},
我们发现computed里的函数差不多, 能否使用一个统一的写法
vuex团队也给提供了4个辅助函数
- mapState
- mapGetters
- mapMutations
- mapActions
2 mapState
为组件创建计算属性以返回 Vuex store 中的状态
先导入mapState
等辅助函数
import { mapState } from 'vuex'
返回一个对象, 包含了计算属性函数
mounted() {
// 返回一个对象
console.log(mapState(['count', 'name', 'age']))
},
...
对象的展开语法
使用展开运算符...
将对象展开完成方法的合并
computed: {
// 使用对象的解构赋值
...mapState(['count', 'name', 'age']),
},
对象映射写法
如果希望计算属性名称和state状态名称不一致, 可以使用对象映射的写法
// mapState(对象写法)
...mapState({ myCount: 'count', myName: 'name', myAge: 'age' }),
3 mapGetters
为组件创建计算属性以返回 getter 的返回值
同理, Vuex也提供了mapGetters
辅助函数
import { mapState, mapGetters } from 'vuex'
在computed中
computed: {
...mapGetters(['doneTodos', 'getTodoById']),
}
4 mapMutations
创建组件方法分发 action
在methods中
...mapMutations({ increment: 'INCREMENT', decrement: 'DECRMENT' }),
注意
这里如果要传参, 只能在调用函数时传递
<button @click="increment(num)">+</button>
<button @click="decrement(num)">-</button>
5 mapActions
创建组件方法提交 mutation
在methods中
...mapActions(['incrementIfOdd', 'incrementAsync', 'complex']),
<button @click="incrementIfOdd(1)">是奇数就+1</button>
<button @click="incrementAsync(1)">异步+1</button>
<script>
// 使用map辅助函数实现
import { mapState, mapMutations, mapActions } from 'vuex'
export default {
name: 'Counter',
data() {
return {
num: 0,
}
},
computed: {
// 对象的展开语法
...mapState(['count']),
},
mounted() {
// mapState返回一个对象
/*
count: function () {
return this.$store.state.count
},
num: function() {
return this.$store.state.num
},
age: function() {
return this.$store.state.age
}
*/
// console.log(mapState(['count', 'num', 'age']))
},
methods: {
// increment() {
// // count: 只是由本组件管理.
// // 希望交给Vuex统一管理
// // console.log(this.$store)
// // 向Vuex分发一个action, 调用对应的actions中的方法
// // this.$store.dispatch('increment')
// this.$store.commit('INCREMENT')
// },
// decrement() {
// // type: actions中的一个方法名字
// // payload: 传递的数据(载荷)
// // this.$store.dispatch('decrement', this.num)
// this.$store.commit('DECREMENT', this.num)
// },
// incrementIfOdd() {
// this.$store.dispatch('incrementIfOdd')
// },
// incrementAsync() {
// this.$store.dispatch('incrementAsync')
// },
...mapMutations({ increment: 'INCREMENT', decrement: 'DECREMENT' }),
// ...mapActions({
// incrementIfOdd: 'incrementIfOdd',
// incrementAsync: 'incrementAsync',
// }),
// 当key和value相同时, 推荐使用数组的写法
...mapActions(['incrementIfOdd', 'incrementAsync']),
},
}
</script>