一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
前言
今天这篇文章的主题是Vuex
,所以看这篇文章之前最好是要有一定Vue
基础的,没有学过Vue
的话我建议先去看看Vue.js
。不过我相信,对于前端小伙伴来说,Vue
框架或多或少都会接触到。好了,话不多说,接下来进入正题:
注意:Vue-cli
的版本不同可能生成的项目目录和方法也不同,无需在意这些。
1.什么是Vuex
?
1.1 官方的解释
Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
1.2 个人总结
看了上面一段文字我想行大多数人都是晕头转向的,官方的话通常都是晦涩难懂的,因为需要保证权威性和正确性。所有我通过我自己的理解给大家简单的理解一下:
所谓的Vuex
其实就是一个为Vue.js
设计的数据仓库,就是把各个组件公用的数据放到一个仓库里面进行统一的管理,这样既使得非父子组件间的数据共享变得简单明了,也让程序变得更加可维护(将数据抽离了出来),而且只要仓库里面的数据发生了变化,在其他组件里面数据被引用的地方也会自动更新。
2.为什么要使用Vuex
?
2.1 组件间传值复杂
如果你之前用过Vue.js
开发过项目,你一定会被各个组件之间的传值搞得晕头转向,特别是非父子组件之间传值时。利用Vuex
我们可以将组件之间共享的数据抽取出来,单独存放在一个store
(仓库)中去,这样各个组件需要数据的时候直接去仓库里面拿就好了,不用组件之间复杂的传值了,而且需要改变数据的时候,只需要将仓库里面的数据更改即可,各个组件里面引用的地方会自动更新。
2.2 Vue
中的单项数据流
与单向数据流对应的就是双向数据流:双向数据流在Vue
中也叫做‘双向绑定’,其实现主要是依靠MVVM
框架,在Vue
中主要由三个部分组成:View
、ViewModel
、Model
。其中View
可以简单的理解为视图层,Model
可以简单的理解为数据层,其中View与Model之间是不能直接通信的,必须得依靠ViewModel中间件来完成。通过ViewModel
就可以实现数据双向绑定,也就是View
与Model
之间的同步是自动的,Model
数据改变了View
视图上的数据也会跟着改变,而不必手动去更新。具体的原理在这里不是重点,就不细说了,感兴趣的小伙伴可以参考:
送上一张图:
Vue
中的单向数据流:理解单项数据流,我们先来简单看一下官网给出的一张图:
我们先来简单理解一下图中三个层说明的什么:View
(视图)、Actions
(响应状态变化,可以简单理解为methods
等等)、State
(数据源,简单理解就是数据)。从图中的箭头我们可以看出,我们通过Actions
改变State
(数据),然后View
(视图)更新,这一个过程是单向的,不会发生View
(视图)改变了,然后State
(数据)更新的情况,这使得我们的数据可控。我们再来看一下官网给出的实例代码,简单明了:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
从上面代码中可以很清楚的反映出图片中的流程。在Vue
中,数据从父组件传递给子组件,只能是单向绑定,子组件内部不能直接修改从父级传过来的数据,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”(ViewModel
)。但是,当我们遇到多个组件同时共享一个状态(数据)时,并且都需要改变状态(数据)时,单项数据的简洁性很容易被破坏,即使我们使用了父子组件的单向数据绑定,但这种模式在这种情况下也会变得非常脆弱,使代码不易维护。
总结出来就以下两个问题容易破坏单向数据流:
多个视图依赖于同一状态。
来自不同视图的行为需要变更同一状态。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
3.什么情况下该使用Vuex?
Vuex
可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 如果您不打算开发大型单页应用,使用Vuex
可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用Vuex
。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex
将会成为自然而然的选择。\
4.构建项目和引入Vuex
前面为什么要花这么大力气去讲这么多基础知识呢?俗话说:‘万丈高楼拔地起’,只有地基打得牢,后面才能进行得更加顺利。说了这么多,现在终于步如正题了,了解了为什么要使用Vuex
,现在我们就一起探讨一下如何使用它:
4.1 构建vue-cli项目
为了方便我们后续的讲解和开发,这里我们使用vue的脚手架工具vue-cli来构建我们的项目,简单列一下操作步骤:
- 开发工具:
VSCode
- 开发环境:
Node.js
环境(npm
包管理工具) - 安装
vue-cli
- 创建
vue-cli
项目
(1)安装vue-cli
执行命令:
npm install -g vue-cli
安装成功:
检查是否安装成功,执行命令:
vue -V
出现版本号则安装成功:
(2)构建vue-cli项目
执行命令:
vue create [项目名称] 或者 vue init webpack(老版本)
如图按需勾选是否安装(router
建议安装):
安装完成后我们的项目结构大致如下样子,使用vue-cli3.X
版本构建的可能会有所不同:
最后检查项目是否能够正常启动:
执行命令:
npm run dev
出现如图则构建成功:
4.2 安装Vuex
并引入项目
(1)安装vuex
执行命令:
npm install vuex --save
如图:
(2)引入vuex
1. 在src
目录下新建一个store
文件夹,并在store
目录下新建index.js
2. 在index.js
里面使用vuex
,添加如下代码:
import Vue from 'vue'
import Vuex from 'vuex'
//使用vuex
Vue.use(Vuex)
//导出store
export default new Vuex.Store({})
3. 在main.js中引入store,然后全局注入一下,这样就可以在任何一个组件里面使用它了:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
//引入store
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
//注册store
store,
components: { App },
template: '<App/>'
})
5.开始使用Vuex
5.1 简单看图理解
在正式开始之前,我们先简单看一张图,这一张图很清晰的描述了使用vuex的整个流程,在这里也不用搞得多明白,心里有个概念就好了,到了后面的代码部分自然就会理解了:
我们可以看到最左边是我们的组件,可以简单理解为我们的视图,虚线框则是我们的vuex
,这里面有一个State
,可以把它理解为数据。以前我们将数据放在组件里面,要修改数据直接在组件里面修改就好了,现在数据放在了vuex
仓库里面,我们要修改数据就得走一定的流程:
-
我们需要在我们组件里面调用
Dispatch()
方法提交Actions
(还记得最开始我们如何说的Actions
吗?)\ -
Actions再通过Commit()方法提交Mutations(简单理解为真正的修改数据的方法)\
-
通过Mutations里面的方法改变state(数据)\
-
响应(渲染)到组件里面。\
5.2 简单理解State
、Actions
、Mutations
看了上面的图可能有一些小伙伴不太理解图里面的State
、Actions
、Mutations
,这里我们就详细理解一下:
state
是什么?
官方的话可能有些晦涩,我们可以简单的理解为:存数据的地方,所有的数据都要存在state里面。这样理解起来就简单多了
Actions
是什么?
Actions和Mutations比较类似,包含的都是一些方法,不同的是Actions不能直接更改数据,它的作用是提交Mutations,Mutations里面包含的才是具体操作数据的方法。
Mutations
是什么?
在vuex
中,唯一能够修改数据的方法就是提交mutation
,简单来说mutations
里面存的就是一些操作数据的方法。
5.3 代码实例
1. 将项目自动生成的HelloWorld.vue
删除,新建三个我们自己的组件,分别是:ChildA.vue
、ChildB.vue
和Parent.vue
,并配置路由:
项目结构变为:
ChildA.vue
<template>
<div class="child-a">
<p>ChildA:{{count}}</p>
<button>ChildA-Add</button>
</div>
</template>
<script>
export default {
name: 'ChildA',
data () {
return {
//我们不再将数据放到组件里
//count: 0
}
}
}
</script>
<style scoped>
</style>
ChildB.vue
<template>
<div class="child-b">
<p>ChildB:{{count}}</p>
<button>ChildB-Add</button>
</div>
</template>
<script>
export default {
name: 'ChildB',
data () {
return {
//count: 0
}
}
}
</script>
<style scoped>
</style>
Parent.vue
<template>
<div class="parent">
<child-a></child-a>
<child-b></child-b>
</div>
</template>
<script>
import ChildA from './ChildA'
import ChildB from './ChildB'
export default {
name: 'Parent',
data () {
return {
}
},
components: {
ChildA,
ChildB
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
修改router
下的index.js
import Vue from 'vue'
import Router from 'vue-router'
import Parent from '@/components/Parent'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Parent',
component: Parent
}
]
})
这个时候我们启动项目,浏览器访问:
我们可以看到,浏览器上面并没有显示{{count}}
的内容,因为我们并没有在组件里面定义count
,考虑到ChildA
和ChildB
公用一个数据count
,所以我么们将count
提出来,利用vuex
的仓库进行管理,所以我们需要在store
目录下里面的index.js
里面定义count
,那么怎么定义呢,我们一起来看看:
修改src/store/index.js
文件代码:
import Vue from 'vue'
import Vuex from 'vuex'
//使用vuex
Vue.use(Vuex)
//导出store
export default new Vuex.Store({
//将数据定义在state里面,state是一个对象
state: {
count: 0
}
})
我们已经定义好了count,那么要怎么在各个组件里面使用这个数据呢?我们可以在组件里面这样调用:
CHildA.vue
export default {
name: 'ChildA',
data () {
return {
//count: 0
}
},
//通过计算属性来获得count
computed: {
count: function(){
//通过vue的this.$store来获得state
return this.$store.state.count
}
}
}
</script>
ChildB.vue
<script>
export default {
name: 'ChildB',
data () {
return {
//count: 0
}
},
computed: {
count(){
return this.$store.state.count
}
}
}
</script>
从上面的代码我们可以很清楚的看到是如何获取到state里面的count的,很多小伙伴可能就有疑惑了,为什么要使用计算属性呢,而不把count定义在data里面,然后通过方法获取呢?在这里我们就要回顾一下vuex里面state的特性了,state状态是响应式的,也就是说如果state里面的count发生了变化,组件里面的数据是自动更新的,所以就需要用到computed属性了。对computed属性不熟的小伙伴可以参考:
- 计算属性:计算属性
关于this.$store
:我们最开始把vuex挂载到了根组件上去,所以我们在任何地方都可以调用$store
,和$router
一个道理,就像是调用store
这个大仓库里面的属性一样。
此时我们看到浏览器页面上已经出现了count
的值:
2.给两个button添加点击事件,以此来更改count的值:
ChildA.vue(ChildB修改代码相同,所以不在这里重复展示)
<template>
<div class="child-a">
<p>ChildA:{{count}}</p>
<button @click="handleClick(10)">ChildA-Add</button>
</div>
</template>
<script>
export default {
name: 'ChildA',
data () {
return {
//count: 0
}
},
computed: {
count: function(){
return this.$store.state.count
}
},
methods: {
handleClick:function(num){
//通过dispatch触发actions中的方法countAdd,actions提交mutations,num是携带的参数
this.$store.dispatch('countAdd',num)
}
}
}
</script>
<style scoped>
</style>
修改src/store/index.js代码:
import Vue from 'vue'
import Vuex from 'vuex'
//使用vuex
Vue.use(Vuex)
//导出store
export default new Vuex.Store({
state: {
count: 0
},
//组件通过dispatch方法触发actions里面的countAdd方法,然后actions提交mutations里面的countAdd方法。
actions: {
//接收组件传过来的参数num,Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象
countAdd(context,num){
context.commit('countAdd',num)
}
},
mutations: {
//传入一个state对象,接收传过来的参数num
countAdd(state,num){
state.count+=num
}
}
})
上面的代码还是比较清晰明了的,较为重要的部分我也添加了注释。在这里我们给button
按钮添加了一个点击事件handleClick()
,这个方法的作用就是增加count的值,增加多少以传入的参数决定,在方法里面我们通过disptch()
方法触发actions
,然后actions
在提交mutations
,最终实现state
数据的更改。在actions
里面我们接收了一个context
对象,可以简单将这个对象理解为store
对象(其实不同)。
最终效果:
我们可以看到,点击任意组件的按钮,页面上的其他组件的count
值也会跟着改变,因为这些组件通过vuex
实现了count
的共享,并且count
的值是自动更新的,有没有觉得很奇妙!
3.直接出发actions
在刚才的组件中,我们通过dispatch
方法出发actions
的,那能不能直接触发actions
呢?答案是可以的,此时我们的handleClick()
方法可以改写为:
methods: {
handleClick:function(num){
//直接调用commit触发actions
this.$store.commit('countAdd',num)
}
}
既然我们直接触发了actions
,那么src/store/index.js
里面的actions
就可以去掉了,因为我们直接commit
了。
那么src/store/index.js
里面的代码就可以简化为:
import Vue from 'vue'
import Vuex from 'vuex'
//使用vuex
Vue.use(Vuex)
//导出store
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
countAdd(state,num){
state.count+=num
}
}
})
启动项目可以发现效果是一样的。
到这里可能就有小伙伴疑问了,为什么不能直接执行mutations里面的方法?这样多简单,答案是不能的,具体原因就不再这里深究了,有兴趣的小伙伴可以参考:Mutations详解
5.4 使用对象展开运算符简化代码
代码写到这里,可能有小伙伴发现:如果我们很多地方需要用到store
里面的数据,那我们就要在很多地方写this.$store。这样看起来代码是十分冗余的。所以为了解决这个问题,我们引入了对象展开运算符mapState
、mapMutations
来简化。
运用mapState
、mapMutations
,我们组件里面的代码就可以这样写:
ChileA.vue(Child.vue里面修改的代码与之类似)
<template>
<div class="child-a">
//使用新的countA
<p>ChildA:{{countA}}</p>
<button @click="handleClick(10)">ChildA-Add</button>
</div>
</template>
<script>
//要想使用展开运算符,就要先引入
import { mapState, mapMutations } from 'vuex'
export default {
name: 'ChildA',
data () {
return {
}
},
computed: {
//通过mapState获得state里面的count,并赋值给countA
...mapState({
countA: 'count'
})
},
methods: {
handleClick:function(num){
//this.$store.commit('countAdd',num)
this.countAdd(num)
},
//通过展开运算符提交mutations里面的方法countAdd
...mapMutations(['countAdd'])
}
}
</script>
<style scoped>
</style>
看到这里有没有觉得代码简洁了不少呢,没有烦人的this.$store,当state里面的数据更多的时候,更能体现出此种方法的优势。
展开运算符是ES6的语法,不太了解的小伙伴可以参考:
在这里我们只用到了mapState
和mapMutations
,其实还有其他对象展开符,比如mapActions
等等,在这里就不深究了。
5.5 Getter与Module
文章写到这儿,vuex基础知识应该差不多了,虽然我们的代码很简单,但是我们需要理解其中的原理,理解其中每一个知识点,比如对象展开符等等,只有理解透彻了,vuex才算是上手了。
下面我们简单讲解一下vuex里面的Getter和Module模块思想,这里我们粗略的过一遍,深入探讨我们放在下一篇文章。
- Getter
可以简单的理解为store的计算属性,就和computed差不多。mapGetters
辅助函数仅仅是将 store
中的 getter
映射到局部计算属性,也就是说我们可以在组件里面使用与mapState类似的使用方法去使用。
大家可以参考:
具体内容我放在下一篇文章
- Module
Module其实很简单,我们只要看一下官网的以下代码就明白了:
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 的状态
这个代码很明了,就是一个分模块的思想,之前我们都是用的单一的store,但是一旦项目很庞大的时候,一个store就会显得非常复杂,所以我们就使用的分模块的思想。具体内容看一下官网就能明白了:
6.总结
整篇文章写下来,我对vuex又有了更加深入的理解,我总结了几点(也不算总结),算是一个知识点的唤醒吧:
- vuex:状态共享
- 为什么要使用vuex
- store:共享仓库
- state:数据存储
- dispatch、actions、mutations
- 数据修改流程
- 对象展开符
- getter:store的计算属性
- module:模块化思想
如果对于上面的几点在心里都有数,那么vuex肯定是入门了的,后面深究就要靠自己了,文章是一个引子,真正的应用还得靠自己。在文章中应该会有许多的不足之处,欢迎各位小伙伴指正我的错误。
再见!下篇文章见!
最后放上一张图供大家饭后思考: