Vuex概述
Vuex是一个专为Vue.js应用程序开发的状态管理模式- 采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
- 同时也是一种组件间通信的方式,且适用于任意组件间通信
状态管理模式
new Vue({
// state
data () {
return { count: 0 }
},
// view
template: `<div>{{ count }}</div>`,
// actions
methods: {
increment () {
this.count++
}
}
})
-
状态自管理应用包含以下几个部分
- state: 驱动应用的数据源
- view: 将
state映射到视图 - actions: 响应
view上状态变化
-
单一的数据流向如下图所示:
-
当应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏,例如:
-
多个视图依赖于同一状态
- 对于多层嵌套的组件,状态传递变得繁琐
- 对于兄弟关系组件,状态传递无能为力
-
来自不同视图的行为需要变更同一状态
- 采用父子组件直接引用或者通过事件来变更状态,导致代码难以维护
-
Vuex的背后思想
- 把组件的共享状态抽取成一个
Store,以全局单例模式管理 - 在该模式下,组件树构成了一个巨大的视图,不管在树的哪个位置,任何组件都能获取状态或者触发行为
- 通过定义和隔离状态管理的概念,并强制规则维持视图和状态间的独立性,代码变得结构化、易维护
- 与其他模式(如
Redux、Flux等)不同,Vuex利用Vue的细粒度数据响应机制来进行高效的状态更新
为什么要使用 Vuex
- 开发单页应用时,经常遇到一些组件间的共享数据或状态,在应用规模较小的时候,需要通过
props或者自定义事件进行传递,或是通过事件总线来进行任意两个组件的通信 - 当应用逐渐复杂后,这样的通信方式会导致数据流异常地混乱,如下图所示
- 当应用到一定的复杂度后,作为开发者该考虑如何更好地管理组件状态,
Vuex自然而然成为最佳选择 - 如果应用够简单,最好不要使用
Vuex,否则会变得繁琐冗余
Vuex工作原理
-
Vuex由五大模块 + 辅助函数构成-
State: 存储的单一状态,是存储的基本数据
-
Getters:
Store的计算属性,对 State 的数据加工- 类似计算属性,Getter 返回的值会根据依赖进行缓存,只有当依赖值变化才会重新计算
-
Mutations: 提交更改数据,更改 State 存储的状态(同步函数)
-
Actions: 装饰器,提交 Mutations,而不是直接变更状态(可异步操作)
-
Module:
Store分割的模块,每个模块拥有独立的 State、Getters、Mutations、Actions -
辅助函数:
Vuex提供mapState、MapGetters、MapActions、mapMutations函数处理Store
-
在 Vue 中触发点击事件,就能触发 methods 中的方法,而在 Vuex 中则需要中间件才能触发
- 在组件中通过
dispatch触发 Actions 中的方法,将事件行为传递给 Actions - Actions 中需要
commit提交,触发 Mutations 中的方法 - Mutations 收到 Actions 中的提交,进而实现 State 中状态/数据的更新
- 如果需要对 State 中的数据进行加工处理,则通过 Getters 进行操作
注意:
Vue2中建议所有异步都在 Actions 中进行,不建议在 Mutations 中处理
-
假如比喻成生活中的例子: Vue Component 是客人,Actions 是服务员,Mutations 是后厨,State 是菜,
Vuex是餐厅,餐厅老板是Store,但是在餐厅看不到- 餐厅来了一位客人,开口呼叫服务员( Actions ),即 Vue Component 调用
dispatch - 客人点餐点了蛋炒饭,只要一份,即
dispatch('danChaoFan',1) - 服务员( Actions )进行确认,即执行
danChaoFan(),参数为1 - 服务员将菜单交给后厨( Mutations ),即
Commit('danChaoFan',1) - 后厨( Mutations )进行加工,把菜( State )做好,然后送到客人( Vue Component )面前(
render) - 如果客人对菜( State )的口味不满意,那么可以加调料( 操作Getters )
- 有时服务员( Actions)可省略,客人( Vue Component )跟后厨( Mutations )可以直接对话(
Commit) - 餐厅必须有个老板( Store ),它是管理者
- 餐厅日益兴旺,开了很多家连锁店( 分割模块 Module ),而且每个分店都有独立的架构
- 餐厅来了一位客人,开口呼叫服务员( Actions ),即 Vue Component 调用
搭建Vuex环境
-
安装
VuexVue2只能使用Vuex@3,Vue3需要安装Vuex@4
npm i vuex@3 --save
- 配置
store
- 在
store文件夹中的index.js配置actions、mutations、state等
// 引入Vuex和Vue
import Vuex from 'vuex'
import Vue from 'vue'
// 挂载Vuex
Vue.use(Vuex)
// 准备Actions---用于响应组件中的动作
const actions = {}
// 准备mutations---用于操作数据
const mutations = {}
// 准备state---用于存储数据
const state = {}
// 创建store,并对外暴露
export default new Vuex.Store({
actions,
mutations,
state
})
注意:创建
store之前必须先use(Vuex)插件,否则会报Uncaught Error
- [vuex] must call Vue.use(Vuex) before creating a store instance.
- 引入
Store,挂载到Vue实例中
- 在
main.js中引入store
- 在
new Vue()时添加store属性,将共享到全局组件以及Vue实例上
import store from './store'
new Vue({
render: h => h(App),
store,
}).$mount('#app')
Vuex核心概念
Vuex的核心概念分别为Store、State、Actions、Mutations、Getters、Modules
Store
-
Vuex应用的核心就是store(仓库),store是一个容器,包含应用中大部分的状态 -
Vuex和单纯的全局对象有以下两点不同:①
Vuex的状态存储是响应式的- 当
store中的状态改变,组件也会相应地高效更新
②不能直接改变
store中的状态- 改变
store中的状态的唯一途径就是显式地提交 (commit) ,从而跟踪每一个状态的变化
- 当
-
提供一个简单的
state和mutations
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const mutations = {
increment(state) {
console.log('更新状态');
state.count++
}
}
const state = {
conut:0
}
export default new Vuex.Store({
mutations,
state
})
- 在
Vue组件中访问this.$store属性 - 现在可以通过
$store.state获取状态对象,通过$store.commit方法触发状态变更
<div id="app" >
<h1>{{$store.state.count}}</h1>
<button @click="changeCount">修改store中的状态</button>
</div>
export default {
methods:{
changeCount(){
this.$store.commit('increment')
}
}
}
为什么需要通过
mutation的方式去改变状态,而不是直接修改?
- 通过提交
mutation的方式,而非直接改变状态,是因为这样可以明确地追踪到状态的变化 - 这个约定能够让意图更加明显,能容易解读应用内部的状态改变
State
-
由于
Vuex是单一状态树,用一个对象包含了全部的应用层级状态,所有state便作为唯一数据源 -
单一状态树能直接定位任一特定的状态片段,也能轻易取得当前应用状态的变更记录
获得 Vuex 状态
- 由于
Vuex的状态存储是响应式的,读取状态最好就是在计算属性中返回某个状态 - 每当
state的状态变化,都会重新求取计算属性,并且触发更新相关联的DOM
<h1>{{count}}</h1>
computed: {
count () {
return this.$store.state.count
}
}
辅助函数mapState
- 当组件需要获取多个状态时,假如都声明为计算属性会显得冗余
- 可以使用
mapState辅助函数生成计算属性
<h1>count:{{count}}</h1>
<h1>countAlias:{{countAlias}}</h1>
<h1>countPlus:{{countPlus}}</h1>
computed: mapState({
// 箭头函数写法
count: state => state.count,
// 传字符串参数 'count'
countAlias:'count',
// 使用常规函数,参数为state
countPlus(state){
return state.count + 2
}
}),
- 当映射的计算属性与
state中的状态同名,可以给mapState传一个字符串数组
computed: mapState(['count']),
- 由于
mapState函数返回的是一个对象,可以通过对象展开运算符将它与局部计算属性混合使用
computed: {
...mapState(['count'])
// 其他计算属性
othersComputed...
},
mapState原理
mapState用到了normalizeNamespace方法,返回一个接受namespace和map参数的函数- 该函数主要用来统一处理成标准格式的
namespace和map数据
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
- 其实
mapState函数相当于fn(namespace, map) mapState函数源码如下:
function (namespace, states) {
var res = {};
// state相当于传入的对象或数组,如mapState(['count']),若不是则抛出错误
if ((process.env.NODE_ENV !== 'production') && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object');
}
// 通过normalizeMap函数将states转化为正常的Map,然后进行遍历,重新定义了res对象
// 将遍历Map的key值对应的value赋给res对象,最后将其返回
normalizeMap(states).forEach((ref)=>{
var key = ref.key;
var val = ref.val;
res[key] = function mappedState () {
var state = this.$store.state;
var getters = this.$store.getters;
if (namespace) {
var module = getModuleByNamespace(this.$store, 'mapState', namespace);
if (!module) return;
state = module.context.state;
getters = module.context.getters;
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
};
res[key].vuex = true;
});
return res
}
normalizeMap作用是将传入的map转化成一个对象数组,如[{key: 'count', val: 'count'}]
function normalizeMap (map) {
if (!isValidMap(map)) return [];
return Array.isArray(map)
? map.map(key => ({key: key, val: key}))
: Object.keys(map).map(key => ({ key: key, val: map[key] }))
}
总结:
mapState本质还是函数,执行过程如下:
- 先遍历由
state对象转化成的Map- 然后判断每个
key对应的val是否为函数,若是函数则将其执行后的值更新到val上- 最后重新组合成一个新对象返回
Getters
Getters可以认为是store的计算属性,其本质和computed一样,会根据它的依赖被缓存起来- 作用: 从
store中的state中派生出一些状态 - 参数: 接受
state作为第一个参数,可以接受其他getter作为第二个参数
const state = {
count:1
}
const getters = {
formatCount:state=>state.count * 2,
addConut:(state,getters)=>state.count + getters.formatCount
}
export default new Vuex.Store({
actions,
mutations,
state,
getters // 把getter也暴露
})
通过属性访问
Getter会暴露为$store.getters对象,可以在任何组件中使用
<h1>formatCount:{{formatCount}}</h1>
<h1>addConut:{{addConut}}</h1>
computed: {
formatCount(){
return this.$store.getters.formatCount
},
addConut(){
return this.$store.getters.addConut
}
}
注意:
getter在通过属性访问时会作为Vue响应式系统缓存的一部分
通过方法访问
- 通过让
getter返回一个函数,来实现给getter传参
const state = {
todoList:[
{ id: 1, task:'study', done: true },
{ id: 2, task:'eat', done: true },
{ id: 3, task:'sleep', done: false }
]
}
const getters = {
getTodoById: (state) => (id) => state.todoList.find(todo => todo.id === id)
}
- 在组件中进行使用
<h1>任务:{{todoatask.task}}</h1>
<h1>是否完成:{{todoatask.done?'已完成':'待完成'}}</h1>
computed: {
todoatask(){
return this.$store.getters.getTodoById(1)
}
},
辅助函数mapGetters
- 仅仅是将
store中的getter映射到局部计算属性 - 原理和
mapState大致相同,最后返回一个对象
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['formatCount','addConut','getTodoById']),
todoatask(){
return this.getTodoById(1)
}
},
- 将
getter属性重新取名字,需使用对象形式
<h1>任务:{{taskList(1).task}}</h1>
<h1>是否完成:{{taskList(1).done?'已完成':'待完成'}}</h1>
...mapGetters({
taskList: 'getTodoById'
})
Mutations
mutation是更改store中的状态的唯一方法mutation类似于事件,每个mutation都有一个事件类型和回调函数- 参数: 接受
state作为第一个参数
const mutations = {
increment(state) {
state.count++
}
}
const state = {
count: 1
}
mutation类似于事件注册,需要commit进行触发,进而执行相应的回调
this.$store.commit('increment') // 触发mutations中的increment方法
提交载荷
- 向
mutation传入的额外参数,称为matution的载荷
const mutations = {
increment(state,payload) {
console.log('+10');
state.count += payload
}
}
- 在组件中
commit时传入额外参数
<div id="app" >
<h1>{{count}}</h1>
<button @click="changeCount">修改store中的状态</button>
</div>
methods:{
changeCount(){
this.$store.commit('increment',10)
}
}
建议:如果存在多个载荷,那么
payload最好为对象,这样可以包含多个属性并且存在记录
commit还可以是对象风格的方式
methods:{
changeCount(){
this.$store.commit({
type:'increment',
num:10
})
}
}
- 这样整个对象都作为载荷传给
mutation函数
const mutations = {
increment(state,payload) {
state.count += payload.num // payload为{ type:'increment',num:10 }
}
}
遵守 Vue 的响应式
-
由于
Vuex的store中的状态是响应式的,那么当状态变更时,监视状态的组件也要自动更新 -
这相当于和
data的使用方式一模一样,遵循响应式的注意事项-
提前在
state中初始化好所需的属性 -
需要在对象中添加/删除新属性时,或者操作数组某一项时
- 使用
Vue.set() - 利用对象展开运算符
state.obj = { ...state.obj, newKey: val }
- 使用
-
为什么需要遵循
Vue的响应式规则?
vuex其实就是一个重新封装的Vue实例对象,其动态响应数据就是data属性,而commit这些API都是回调而已
辅助函数mapMutations
- 使用
mapMutations辅助函数将组件中的methods映射为store.commit调用 - 在原理上和
mapState大致相同
methods:{
// 将 `this.increment()` 映射为 `this.$store.commit('increment')`
...mapMutations(['increment']),
changeCount(){
this.increment(10)
}
}
Actions
-
action与mutation的不同在于:action提交的是mutation,而不是直接变更状态- 可以包含任意异步操作
-
注册一个简单的
action -
参数:
- 第一个参数:与
store实例具有相同方法和属性的context对象,但不是其本身 - 第二个参数:与
mutation中一样的载荷
- 第一个参数:与
-
可以通过
context.state和context.getters来获取state和getters
// 正常写法
const actions = {
incrementAction (context,payload) {
console.log('actions触发');
context.commit('increment',payload) // 触发mutation
}
}
// 解构写法
const actions = {
incrementAction ({commit},payload) {
console.log('actions触发');
commit('increment',payload)
}
}
分发 Action
- 通过
store.dispatch方法触发
<div id="app" >
<h1>{{count}}</h1>
<button @click="changeCount">修改store中的状态</button>
</div>
methods:{
changeCount(){
this.$store.dispatch('incrementAction',10)
}
}
action内部可以执行异步操作- 这样可以精准获取到
mutation中的状态变更时机
const actions = {
incrementAction ({commit},payload) {
console.log('actions触发');
setTimeout(() => {
commit('increment',payload)
}, 5000);
}
}
辅助函数mapActions
- 使用
mapActions辅助函数将组件的 methods 映射为$store.dispatch调用
methods:{
// 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
...mapActions(['incrementAction']),
changeCount(){
this.incrementAction(10)
}
}
组合Action
$store.dispatch可以处理action的回调函数返回的Promise
const actions = {
actionA ({ commit },payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment',payload)
resolve('action触发完成')
}, 1000)
})
}
}
- 在组件中使用
then链式调用
methods:{
// 将 `this.actionA()` 映射为 `this.$store.dispatch('actionA')`
...mapActions(['actionA']),
changeCount(){
this.actionA(10).then(res=>{console.log(res);})
}
}
- 可以利用
async / await组合action
const actions = {
actionA ({ commit },payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment',payload) // actionA中 commit 触发一次mutation
resolve('actionA触发完成')
}, 1000)
})
},
async actionB ({ dispatch, commit },payload) {
const res = await dispatch('actionA',payload) // 等待 actionA 完成
console.log(res);
commit('increment',payload) // actionB也触发一次mutation
}
}
- 在组件中触发
actionB
methods:{
// 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
...mapActions(['actionB']),
changeCount(){
this.actionB(10)
}
}
Modules
未完待更... ...