Vue系列之—Vuex详解
一、Vuex概述
1.1 官方解释
Vuex
是一个专为 Vue.js
-
它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化 -
Vuex
也集成到Vue
的官方调试工具devtools extension
,提供了诸如零配置的time-travel
调试、状态快照导入导出等高级调试功能。
1.2 个人理解
状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人有点捉摸不透。
其实,可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。
然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。
那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?
如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?
当然可以,只是我们要先想想Vue.js
带给我们最大的便利是什么呢?没错,就是响应式。
如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。
Vuex
就是为了提供这样一个在多个组件间共享状态的插件。
1.3 组件间共享数据的方式
-
父向子传值:
v-bind
属性绑定 -
子向父传值:
v-on
事件绑定 -
兄弟组件之间共享数据:
EventBus
-
$on
接收数据的组件 -
$emit
发送数据的组件
上述只适合小范围内数据共享,如果是复杂应用,就不再合适。
1.4 Vuex是什么
Vuex
是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享
如图:
在不使用Vuex进行状态管理时,如果要从最下面的紫色组件传递数据的话,还是比较繁琐,也不便于维护。
在使用Vuex
进行状态管理时,只需要一个共享Store
组件,紫色组件将数据写入Store
中,其他使用的组件直接从Store
中读取即可。
1.5 使用Vuex统一管理好处
-
能够在
Vuex
中集中管理共享的数据,易于开发和后期维护 -
能够高效地实现组件之间的数据共享,提高开发效率
-
存储在
Vuex
中的数据都是响应式的,能够实时保持数据与页面的同步
二、状态管理
2.1 单页面状态管理
我们知道,要在单个组件中进行状态管理是一件非常简单的事情,如图:
-
State:指的就是我们的状态,可以暂时理解为组件中
data
中的属性 -
View:视图层,可以针对
State
的变化, 显示不同的信息 -
Actions:这里的
Actions
主要是用户的各种操作,如点击、输入等,会导致状态发生变化
简单加减法案例,代码如下:
<template> <div> <div>当前计数为:{{counter}}</div> <button @click="counter+=1">+1</button> <button @click="counter-=1">-1</button> </div> </template>
<script> export default { name: "HelloWorld", data() { return { counter: 0 }; } }; </script>
在这个案例中,有没有状态需要管理呢?肯定是有的,就是个数counter
counter
需要某种方式被记录下来,也就是上述中的的State
部分 counter
的值需要被显示在洁面皂,这个就是上述中的View
部分 界面发生某些操作(比如此时的+1、-1),需要去更新状态,这就是上述中的Actions
部分 这就是一个最基本的单页面状态管理。
2.2 多页面状态管理
Vue
已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢,比如:多个视图
View都依赖同一个状态(一个状态改了,多个界面需要进行更新) 不同界面的Actions
都想修改同一个状态 也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个视图,但是也有一些状态(状态a/状态b/状态c)属于多个试图共同想要维护的,那怎么办呢?
状态1/状态2/状态3你放在自己的组件中,自己管理自己用,没问题 但是状态a/状态b/状态c我们希望交给一个大管家来统一帮助我们管理 没错,Vuex
就是为我们提供这个大管家的工具。
2.3 全局单例模式
我们现在要做的就是将共享的状态抽出来,交给我们的大管家,统一进行管理,每个视图按照规定,进行访问和修改操作。
这就是Vuex
的基本思想
2.4 管理哪些状态
如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。
比如用户的登录状态、用户名称、头像、地理位置信息等 比如商品的收藏、购物车中的物品等 这些状态信息,我们都可以放在统一放在Vuex中,对它进行保存和管理,而且它们还是响应式的。
一般情况下,只有组件之间共享的数据,才有必要存储到Vuex中。
对于组件中的私有数据,依旧存储在组件自身的data中即可。
三、Vuex的基本使用
3.1 安装
npm install vuex --save
3.2 导入
import Vuex from 'vuex'
Vue.use(Vuex)
3.3 创建store对象
const store = new Vuex.Store({ // state中存放的就是全局共享数据 state:{ count: 0 } })
3.4 挂载store对象
new Vue({ el: '#app', render: h=>h(app)m router, //将创建的共享数据对象,挂载到Vue实例中 //所有的组件,就可以直接从store中获取全局的数据了 store })
3.5 创建带vuex的vue项目
3.5.1 创建过程
-
打开终端,输入命令:vue ui
-
当项目仪表盘打开之后,我们点击页面左上角的项目管理下拉列表,再点击Vue项目管理器
-
点击创建项目
-
设置项目名称和包管理器
-
设置手动配置项目
-
设置功能项
-
创建项目
3.5.2 项目代码格式化
在项目根目录(与src平级)中创建 .prettierrc 文件,编写代码如下:
{ "semi":false, "singleQuote":true }
四、Vuex的核心概念
4.1 State
4.1.1 概念
Sta
te是提供唯一的公共数据源,所有共享的数据都要统一放到`Store的State中进行存储。
如果状态信息是保存到多个Store对象中的,那么之后的管理和维护等都会变得特别困难,所以Vuex也使用了单一状态树(单一数据源Single Source of Truth)来管理应用层级的全部状态。
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。
export default new Vuex.Store({ state: { count: 0 } }
4.1.2 State数据访问方式一
通过this.$store.state.全局数据名称
访问,eg.
<h3>当前最新Count值为:{{this.$store.state.count}}</h3>
4.1.3 State数据访问方式二
从vuex
中按需导入mapState
函数
import { mapState } from 'vuex'
通过刚才导入的mapState
函数,将当前组件需要的全局数据,映射为当前组件的computed
计算属性:
<template> <div> <h3>当前最新Count值为:{{ count }}</h3> <button>-1</button> </div> </template> <script> import { mapState } from "vuex"; export default { computed: { ...mapState(["count"]) } }; </script>
4.2 Mutation
4.2.1 引入
如果想修改count
的值,要怎么做呢?
也许聪明的你,已经想到,直接在组件中对this.$store.state.count
进行操作即可,代码如下
<template> <div> <h3>当前最新Count值为:{{this.$store.state.count}}</h3> <button @click="add">+1</button> </div> </template> <script> export default { methods: { add() { this.$store.state.count++; } } }; </script>
测试发现,这可以实现需求,完成+1
操作。
但是,这种方法在vuex
中是严格禁止的,那要怎么做呢?这时,就需要使用Mutation
了。
4.2.2 概念
Mutation
用于变更存储在Store
中的数据。
-
只能通过
mutation
变更Store
数据,不可以直接操作Store
中的数据 -
通过这种方式,虽然操作稍微繁琐一些,但可以集中监控所有数据的变化,直接操作
Store
数据是无法进行监控的
4.2.3 定义Mutation函数
在mutations
中定义函数,如下:
mutations: { // 自增 add(state) { state.count++ } }
定义的函数会有一个默认参数state
,这个就是存储在Store
中的state
对象。
4.2.4 调用Mutation函数
Mutation中不可以执行异步操作,如需异步,请在Action中处理
4.2.4.1 方式一
在组件中,通过this.$store.commit(方法名)
完成触发,如下:
mutations: { // 自增 add(state) { state.count++ } }
4.2.4.2 方式二
在组件中导入mapMutations函数
import { mapMutations } from 'vuex'
通过刚才导入的mapMutations函数,将需要的mutations函数映射为当前组件的methods方法:
methods:{ ...mapMutations('add','addN'), // 当前组件设置的click方法 addCount(){ this.add() } }
4.2.5 Mutation传递参数
在通过mutation
更新数据的时候,有时候需携带一些额外的参数,此处,参数被成为mutation
的载荷Payload
。
如果仅有一个参数时,那payload
对应的就是这个参数值,eg.
如果是多参数的话,那就会以对象的形式传递,此时的payload
是一个对象,可以从对象中取出相关的数据。
在mutations
中定义函数时,同样可以接收参数,示例如下:
mutations: { // 自增 add(state) { state.count++ }, // 带参数 addNum(state, payload) { state.count += payload.number } }
在组件中,调用如下:
methods: { add() { // this.$store.state.count++; this.$store.commit("add"); }, addNum() { this.$store.commit("addNum", { number: 10 }); } }
4.2.6 Mutation响应规则
Vuex
的store
中的State
是响应式的,当State
中的数据发生改变时,Vue
组件也会自动更新。
这就要求我们必须遵守一些Vuex
对应的规则:
-
提前在
store
中初始化好所需的属性 -
当给 State 中的对象添加新属性时,使用如下方式:
-
使用
Vue.set(obj,'newProp','propValue')
-
用新对象给旧对象重新赋值
示例代码:
updateUserInfo(state) { // 方式一 Vue.set('user', 'address', '北京市') // 方式二 state.user = { ...state.user, 'address': '上海市' } }
-
4.2.7 Mutation常量类型
4.2.7.1 引入
思考一个问题:
在mutation中, 我们定义了很多事件类型(也就是其中的方法名称),当项目越来越大时,Vuex管理的状态越来越多,需要更新状态的情况也越来越多,也就意味着Mutation中的方法越来越多。
当方法过多,使用者需要花费大量时间精力去记住这些方法,甚至多个文件间来回切换,查看方法名称,也存在拷贝或拼写错误的情况。
那么该如何避免呢?
-
在各种Flux实现中,一种很常见的方案就是使用常量替代
Mutation
事件的类型 -
可以将这些常量放在一个单独的文件中,方便管理,整个
App
所有的事件类型一目了然
4.2.7.1 解决方案
-
创建
mutation-types.js
文件,在其中定义常量 -
定义常量时, 可以使用
ES2015
中的风格, 使用一个常量来作为函数的名称 -
使用处引入文件即可
新建mutation-types.js
:
在store/index.js
中引入并使用:
import Vue from 'vue' import Vuex from 'vuex' import * as types from './mutation-type' Vue.use(Vuex) export default new Vuex.Store({ state: { count: 0, user: { name: '旺财', age: 12 } }, mutations: { // 自增 [types.ADD_NUM](state) { state.count++ }, }
在组件中,引入并调用:
<script> import { ADD_NUM } from "../store/mutation-type"; export default { methods: { add() { this.$store.commit(ADD_NUM); // this.addAsync(); // this.$store.state.count++; // this.$store.commit("add"); } } }; </script>
4.3 Action
Action
类似于Mutation
,但是是用于处理异步任务的,比如网络请求等
如果通过异步操作变更数据,必须通过Action
,而不能使用Mutation
,但在Action
中还是要通过触发Mutation
的方式间接变更数据。
4.3.1 参数context
在actions
中定义的方法,都会有默认值context
。
-
context
是和store
对象具有相同方法和属性的对象 -
可以通过
context
进行commit
相关操作,可以获取context.state
数据
但他们并不是同一个对象,在Modules
中会介绍到区别。
4.3.2 使用方式一
在index.js
中,添加actions
及对应的方法:
export default new Vuex.Store({ state: { count: 0 }, //只有 mutations 中定义的函数,才有权力修改 state 中的数据 mutations: { // 自增 add(state) { state.count++ } }, actions: { addAsync(context) { setTimeout(() => { //在 action 中,不能直接修改 state 中的数据 //必须通过 context.commit() 触发某个 mutation 才行 context.commit('add') }, 1000); } } })
在组件中调用:
<script> export default { methods: { addNumSync(){ // dispatch函数 专门用于触发 Action this.$store.dispatch('addAsync') } } }; </script>
4.3.3 使用方式二
在组件中,导入mapActions
函数
import { mapActions } from 'vuex'
通过刚才导入的mapActions
函数,将需要的actions
函数映射为当前组件的methods
方法:
<script> import { mapActions } from "vuex"; export default { methods: { ...mapActions(["addAsync"]), add() {Î this.addAsync() }, }
4.3.4 使用方式三
在导入mapActions
后,可以直接将指定方法绑定在@click
事件上。
...mapActions(["addAsync"]),
---------------------------
<button @click="addAsync">+1(异步)</button>
该方式也适用于导入的mapMutations
4.3.5 Actions携带参数
在index.js
的actions
中,增加携带参数方法,如下:
export default new Vuex.Store({ state: { count: 0 }, mutations: { // 带参数 addNum(state, payload) { state.count += payload.number } }, actions: { addAsyncParams(context, payload) { setTimeout(() => { context.commit('addNum', payload) }, 1000); } } })
在组件中,调用如下:
methods: { addNumSyncParams() { this.$store.dispatch("addAsyncParams", { number: 100 }); } }
4.3.6 Actions与Promise结合
Promise
经常用于异步操作,在Action
中,可以将异步操作放在Promise
中,并且在成功或失败后,调用对应的resolve
或reject
。
示例:
在store/index.js
中,为actions
添加异步方法:
actions: { loadUserInfo(context){ return new Promise((resolve)=>{ setTimeout(() => { context.commit('add') resolve() }, 2000); }) } }
在组件中调用,如下:
methods: { addPromise() { this.$store.dispatch("loadUserInfo").then(res => { console.log("done"); }); } }
4.4 Getter
-
Getters
用于对Store
中的数据进行加工处理形成新的数据,类似于Vue
中的计算属性 -
Store
中数据发生变化,Getters
的数据也会跟随变化
4.4.1 使用方式一
在index.js
中定义getter
//定义 Getter const store = new Vuex.Store({ state:{ count: 0 }, getters:{ showNum(state){ return '当前Count值为:['+state.count']' } } })
在组件中使用:
this.$store.getters.名称
<h3>{{ this.$store.getters.showNum }}</h3>
4.4.2 使用方式二
在组件中,导入mapGetters
函数
import { mapGetters } from 'vuex'
通过刚才导入的mapGetters
函数,将需要的getters
函数映射为当前组件的computed
方法:
computed: { ...mapGetters(["showNum"]) }
使用时,直接调用即可:
<h3>{{ showNum }}</h3>
4.5 Modules
4.5.1 概念
Module
是模块的意思,为什么会在Vuex
中使用模块呢?
-
Vues
使用单一状态树,意味着很多状态都会交给Vuex
来管理 -
当应用变的非常复杂时,
Store
对象就可能变的相当臃肿 -
为解决这个问题,
Vuex
允许我们将store
分割成模块(Module)
,并且每个模块拥有自己的State、Mutation、Actions、Getters
等
4.5.2 使用
在store
目录下,新建文件夹modules
,用于存放各个模块的modules
文件,此处以moduleA
为例。
在modules
文件夹中,新建moduleA.js
,内部各属性state
、mutations
等都和之前一致,注释详见代码,示例如下:
export default { state: { name: '凤凰于飞' }, actions: { aUpdateName(context) { setTimeout(() => { context.commit('updateName', '旺财') }, 1000); } }, mutations: { updateName(state, payload) { state.name = payload } }, getters: { fullName(state) { return state.name + '王昭君' }, fullName2(state, getters) { // 通过getters调用本组方法 return getters.fullName + ' 礼拜' }, fullName3(state, getters, rootState) { // state代表当前module数据状态,rootState代表根节点数据状态 return getters.fullName2 + rootState.counter } } }
-
局部状态通过
context.state
暴露出来,根节点状态则为context.rootState
在store/index.js
中引用moduleA
,如下:
import Vue from "vue" import Vuex from "vuex" import moduleA from './modules/moduleA' Vue.use(Vuex) const store = new Vuex.Store({ modules: { a: moduleA } }) export default store
这样就通过分模块完成了对状态管理的模块化拆分。
4.6 优化
如果项目非常复杂,除了分模块划分外,还可以将主模块的actions
、mutations
、getters
等分别独立出去,拆分成单独的js
文件,分别通过export
导出,然后再index.js
中导入使用。
示例: 分别将主模块的actions
、mutations
、getters
独立成js
文件并导出,以actions.js
为例,
export default{ aUpdateInfo(context, payload) { return new Promise((resolve, reject) => { setTimeout(() => { context.commit('updateInfo') resolve() }, 1000); }) } }
在store/index.js
中,引入并使用,如下:
import Vue from "vue" import Vuex from "vuex" import mutations from './mutations' import actions from './actions' import getters from './getters' import moduleA from './modules/moduleA' Vue.use(Vuex) const state = { counter: 1000, students: [ { id: 1, name: '旺财', age: 12 }, { id: 2, name: '小强', age: 31 }, { id: 3, name: '大明', age: 45 }, { id: 4, name: '狗蛋', age: 78 } ], info: { name: 'keko' } } const store = new Vuex.Store({ state, mutations, getters, actions, modules: { a: moduleA } }) export default store
最终项目目录图:
总结
本文以自己能理解的方式详细介绍了Vuex的概念以及核心知识点,再总结一下:
-
Vuex主要用于管理Vue组件中共享的数据。
-
Vuex中有state、mutation、action、getter等核心概念。
-
获取state可以通过this.$store.state.xx或者是通过定义mapState来获取。
-
修改state中的变量需要通过mutation函数实现,而mutation的触发由两种方式,一种是通过this.$store.commit()函数,另外一种就是通过mapMutations来实现。
-
mutation只能用于修改数据,而Actions可以实现异步操作。
-
通过Actions的异步操作+mutation的修改数据,可以实现异步修改数据。调用Actions有两种方式,第一种是通过this.$store.dispatch来调用,另外一种方式是通过mapActions来调用。
-
Getters函数用于对Store中数据进行加工,不会修改原本Store中的数据;Getters中的数据会受Store中数据进行影响。