vue 快速入门 系列 —— Vuex 基础
其他章节请看:
Vuex 基础
Vuex 是 Vue.js 官方的状态管理器
在vue 的基础应用(上)一文中,我们已知道父子之间通信可以使用 props
和 $emit
,而非父子组件通信(兄弟、跨级组件、没有关系的组件)使用 bus(中央事件总线)来起到通信的作用。而 Vuex 作为 vue 的一个插件,解决的问题与 bus 类似。bus 只是一个简单的组件,功能也相对简单,而 Vuex 更强大,使用起来也复杂一些。
现在的感觉就是 Vuex 是一个比 bus 更厉害的东西,可以解决组件之间的通信。更具体些,就是 vuex 能解决多个组件共享状态的需求:
- 多个视图(组件)依赖于同一状态
- 来自不同视图(组件)的行为需要变更同一状态。
Vuex 把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。
环境准备
通过 vue-cli 创建项目
// 项目预设 `[Vue 2] less`, `babel`, `router`, `vuex`, `eslint`
$ vue create test-vuex
Tip:环境与Vue-Router 基础相同
核心概念
Vuex 的核心概念有 State、Getters、Mutations、Actions和Modules。
我们先看一下项目 test-vuex 中的 Vuex 代码:
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// vuex 中的数据
state: {
},
// 更改 vuex 中 state(数据)的唯一方式
mutations: {
},
// 类似 mutation,但不能直接修改 state
actions: {
},
// Vuex 允许将 store 分割成模块(module),每个模块可以拥有自己的 state、mutation、action、getter
modules: {
}
})
Getters,可以认为是 store 的计算属性
State
state 是 Vuex 中的数据,类似 vue 中的 data。
需求:在 state 中定义一个属性 isLogin,从 About.vue 中读取该属性。
直接上代码:
// store/index.js
export default new Vuex.Store({
state: {
isLogin: true
},
})
// views/About.vue
<template>
<div class="about">
<p>{{ this.$store.state.isLogin }}</p>
</div>
</template>
页面输出 true
。
Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)),子组件能通过 this.$store 访问,这样就无需在每个使用 state 的组件中频繁的导入。
// main.js
new Vue({
store,
render: h => h(App)
}).$mount('#app')
// store/index.js
Vue.use(Vuex)
Tip:Vuex 的状态存储是响应式。
mapState 辅助函数
从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态。
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键。
// views/About.vue
<template>
<div class="about">
<p>{{ isLogin }}</p>
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
computed: mapState([
// 映射 this.isLogin 为 store.state.isLogin
'isLogin'
])
}
</script>
页面同样输出 true。
Tip:更多特性请看官网
Getters
Getters,可以认为是 store 的计算属性。
getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
需求:从 isLogin 派生出一个变量,从 About.vue 中读取该属性
直接上代码:
// store/index.js
export default new Vuex.Store({
state: {
isLogin: true
},
getters: {
translationIsLogin: state => {
return state.isLogin ? '已登录' : '未登录'
}
},
})
// views/About.vue
<template>
<div class="about">
<p>{{ this.$store.getters.translationIsLogin }}</p>
</div>
</template>
页面输出“已登录”
Tip:更多特性请参考官网。
- 可以给 getter 传参
- 有与 state 类似的辅助函数,这里是
mapGetters
Mutations
mutation 是更改 vuex 中 state(数据)的唯一方式。
mutation 类似事件,每个 mutation 都有一个字符串的事件类型和 一个回调函数。不能直接调用一个 mutation handler,只能通过 store.commit
方法调用。
需求:定义一个 mutation(更改 isLogin 状态),在 About.vue 中过三秒触发这个 mutation。
直接上代码:
// store/index.js
export default new Vuex.Store({
state: {
isLogin: true
},
mutations: {
toggerIsLogin(state) {
state.isLogin = !state.isLogin
}
},
})
// views/About.vue
<template>
<div class="about">
<p>{{ isLogin }}</p>
</div>
</template>
<script>
export default {
created() {
setInterval(()=>{
this.$store.commit('toggerIsLogin')
}, 3000)
},
}
</script>
页面每三秒会依次显示 true -> false -> true ...
Mutation 必须是同步函数
- 笔者在 mutation 中写异步函数(使用
setTimeout
)测试,没有报错 - 在 mutation 中混合异步调用会导致程序很难调试(使用 devtools)
- 当调用了两个包含异步回调的 mutation 来改变状态,不知道什么时候回调和哪个先回调
结论:在 mutation 中只使用同步函数,异步操作放在 action 中。
Tip:更多特性请参考官网。
- 可以给 mutation 传参
- 触发(
commit
)方式可以使用对象 - 有与 state 类似的辅助函数,这里是
mapMutations
Actions
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
需求:定义一个 action,里面有个异步操作,过三秒更改 isLogin 状态。
直接上代码:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
isLogin: true
},
mutations: {
toggerIsLogin(state) {
state.isLogin = !state.isLogin
}
},
actions: {
toggerIsLogin(context) {
setInterval(() => {
context.commit('toggerIsLogin')
}, 3000)
}
},
})
// views/About.vue
<template>
<div class="about">
<p>{{ isLogin }}</p>
</div>
</template>
<script>
export default {
created() {
// 通过 dispatch 分发
this.$store.dispatch('toggerIsLogin')
},
}
</script>
过三秒,页面的 true 变成 false。
实践中,我们会经常用到 ES2015 的参数解构来简化代码:
actions: {
toggerIsLogin({ commit }) {
setInterval(() => {
commit('toggerIsLogin')
}, 3000)
}
},
Tip:更多特性请参考官网。
- 可以给 Actions 传参
- 触发(
dispatch
)方式可以使用对象 - 有与 state 类似的辅助函数,这里是
mapActions
- 组合多个 Action
Modules
目前我们的 store 都写在一个文件中,当应用变得复杂时,store 对象就有可能变得相当臃肿。
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
Vuex 允许将 store 分割成模块(module),每个模块可以拥有自己的 state、mutation、action、getter。
需求:定义两个模块,每个模块定义一个状态,在 About.vue 中显示这两个状态
直接上代码:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const moduleA = {
state: () => ({ name: 'apple' }),
}
const moduleB = {
state: () => ({ name: 'orange' }),
}
export default new Vuex.Store({
modules: {
a: moduleA,
b: moduleB,
}
})
// views/About.vue
<template>
<div class="about">
<!-- 即使给这两个模块都加上命名空间,这样写也是没问题的 -->
<p>{{ this.$store.state.a.name }} {{ this.$store.state.b.name }}</p>
</div>
</template>
页面显示 “apple orange”。
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。就像这样:
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
}
对于模块内部的 action,局部状态通过 context.state
暴露出来,根节点状态则为 context.rootState
。就像这样:
const moduleA = {
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的。
如果希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。请看示意代码:
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']
}
}
}
}
}
})
小练习
请问 About.vue 会输出什么?(答案在文章底部)
// views/About.vue
<template>
<div class="about">
<p>{{ this.$store.state.a.name }} {{ this.$store.state.b.name }}</p>
<p>
{{ this.$store.getters.nameA }} {{ this.$store.getters.nameB }}
{{ this.$store.getters["b/nameB"] }}
</p>
</div>
</template>
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const moduleA = {
namespaced: true,
state: () => ({ name: 'apple' }),
}
const moduleB = {
namespaced: true,
state: () => ({ name: 'orange' }),
getters: {
nameB: state => `[${state.name}]`
}
}
export default new Vuex.Store({
modules: {
a: moduleA,
b: moduleB,
},
getters: {
nameA: state => state.a.name,
nameB: state => state.b.name
}
})
Tip: 更多特性请参考官网。
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,官网给出了一个项目结构示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
Tip:在笔者即将完成的另一篇文章“使用 vue-cli 3 搭建一个项目”中会有更详细的介绍
附录
小练习答案
apple orange
apple orange [orange]
其他章节请看:
出处:https://www.cnblogs.com/pengjiali/p/15419402.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。