Vue生态圈--Vuex的基本使用

133 阅读8分钟

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 上状态变化
  • 单一的数据流向如下图所示:

image-20221118121158749.png

  • 当应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏,例如:

    1. 多个视图依赖于同一状态

      • 对于多层嵌套的组件,状态传递变得繁琐
      • 对于兄弟关系组件,状态传递无能为力
    2. 来自不同视图的行为需要变更同一状态

      • 采用父子组件直接引用或者通过事件来变更状态,导致代码难以维护

Vuex 的背后思想

  • 把组件的共享状态抽取成一个 Store,以全局单例模式管理
  • 在该模式下,组件树构成了一个巨大的视图,不管在树的哪个位置,任何组件都能获取状态或者触发行为
  • 通过定义和隔离状态管理的概念,并强制规则维持视图和状态间的独立性,代码变得结构化、易维护
  • 与其他模式(如 ReduxFlux 等)不同,Vuex 利用 Vue 的细粒度数据响应机制来进行高效的状态更新

image-20221118143136058.png

为什么要使用 Vuex

  • 开发单页应用时,经常遇到一些组件间的共享数据或状态,在应用规模较小的时候,需要通过 props 或者自定义事件进行传递,或是通过事件总线来进行任意两个组件的通信
  • 应用逐渐复杂后,这样的通信方式会导致数据流异常地混乱,如下图所示

image-20221118150355646.png

  • 当应用到一定的复杂度后,作为开发者该考虑如何更好地管理组件状态,Vuex 自然而然成为最佳选择
  • 如果应用够简单,最好不要使用 Vuex,否则会变得繁琐冗余

Vuex工作原理

  • Vuex五大模块 + 辅助函数构成

    1. State: 存储的单一状态,是存储的基本数据

    2. GettersStore 的计算属性,对 State 的数据加工

      • 类似计算属性,Getter 返回的值会根据依赖进行缓存,只有当依赖值变化才会重新计算
    3. Mutations: 提交更改数据,更改 State 存储的状态(同步函数)

    4. Actions: 装饰器,提交 Mutations,而不是直接变更状态(可异步操作)

    5. Module: Store 分割的模块,每个模块拥有独立的 StateGettersMutationsActions

    6. 辅助函数: Vuex 提供mapStateMapGettersMapActionsmapMutations函数处理 Store

1668789558542.png

Vue 中触发点击事件,就能触发 methods 中的方法,而在 Vuex 中则需要中间件才能触发

  1. 组件中通过 dispatch 触发 Actions 中的方法,将事件行为传递给 Actions
  2. Actions 中需要 commit 提交,触发 Mutations 中的方法
  3. Mutations 收到 Actions 中的提交,进而实现 State 中状态/数据的更新
  4. 如果需要对 State 中的数据进行加工处理,则通过 Getters 进行操作

注意:Vue2 中建议所有异步都在 Actions 中进行,不建议在 Mutations 中处理

  • 假如比喻成生活中的例子: Vue Component 是客人,Actions 是服务员,Mutations 是后厨,State 是菜,Vuex 是餐厅,餐厅老板是Store,但是在餐厅看不到

    1. 餐厅来了一位客人,开口呼叫服务员( Actions ),即 Vue Component 调用 dispatch
    2. 客人点餐点了蛋炒饭,只要一份,即 dispatch('danChaoFan',1)
    3. 服务员( Actions )进行确认,即执行danChaoFan(),参数为1
    4. 服务员将菜单交给后厨( Mutations ),即Commit('danChaoFan',1)
    5. 后厨( Mutations )进行加工,把菜( State )做好,然后送到客人( Vue Component )面前( render )
    6. 如果客人对菜( State )的口味不满意,那么可以加调料( 操作Getters )
    7. 有时服务员( Actions)可省略,客人( Vue Component )跟后厨( Mutations )可以直接对话(Commit)
    8. 餐厅必须有个老板( Store ),它是管理者
    9. 餐厅日益兴旺,开了很多家连锁店( 分割模块 Module ),而且每个分店都有独立的架构

搭建Vuex环境

  1. 安装 Vuex

    • Vue2 只能使用 Vuex@3Vue3 需要安装 Vuex@4
 npm i vuex@3 --save

  1. 配置 store
  • store 文件夹中的 index.js 配置 actionsmutationsstate
 // 引入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.

  1. 引入Store,挂载到 Vue 实例中
  • main.js 中引入 store
  • new Vue() 时添加 store 属性,将共享到全局组件以及 Vue 实例上
 import store from './store'
 new Vue({
   render: h => h(App),
   store,
 }).$mount('#app')

1668791229043.png

Vuex核心概念

  • Vuex 的核心概念分别为 StoreStateActionsMutationsGettersModules

Store

  • Vuex 应用的核心就是 store(仓库)store 是一个容器,包含应用中大部分的状态

  • Vuex 和单纯的全局对象有以下两点不同:

    Vuex 的状态存储是响应式的

    • store 中的状态改变,组件也会相应地高效更新

    ②不能直接改变 store 中的状态

    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) ,从而跟踪每一个状态的变化
  • 提供一个简单的 statemutations

 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')
     }
   }
 }

1668847401731.png

为什么需要通过 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
   }
 }),

1668851379466.png

  • 当映射的计算属性与 state 中的状态同名,可以给 mapState 传一个字符串数组
 computed: mapState(['count']),
  • 由于mapState 函数返回的是一个对象,可以通过对象展开运算符将它与局部计算属性混合使用
 computed: {
   ...mapState(['count'])
   // 其他计算属性
   othersComputed...
 },

mapState原理

  • mapState 用到了 normalizeNamespace 方法,返回一个接受 namespacemap 参数的函数
  • 该函数主要用来统一处理成标准格式的 namespacemap 数据
 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 本质还是函数,执行过程如下:

  1. 先遍历由 state 对象转化成的 Map
  2. 然后判断每个 key 对应的 val 是否为函数,若是函数则将其执行后的值更新到 val
  3. 最后重新组合成一个新对象返回

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)
   }
 },

1668876767779.png

> **注意:`getter` 在通过方法访问时,每次都会调用,不会缓存**

辅助函数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)
   }
 }

1668878546853.png

建议:如果存在多个载荷,那么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 的响应式

  • 由于 Vuexstore 中的状态是响应式的,那么当状态变更时,监视状态的组件也要自动更新

  • 这相当于和 data 的使用方式一模一样,遵循响应式的注意事项

    1. 提前在 state 中初始化好所需的属性

    2. 需要在对象中添加/删除新属性时,或者操作数组某一项时

      • 使用 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

  • actionmutation 的不同在于:

    • action 提交的是 mutation,而不是直接变更状态
    • 可以包含任意异步操作
  • 注册一个简单的 action

  • 参数:

    • 第一个参数:与 store 实例具有相同方法和属性的 context 对象,但不是其本身
    • 第二个参数:与 mutation 中一样的载荷
  • 可以通过 context.statecontext.getters 来获取 stategetters

 // 正常写法
 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)
   }
 }

1668953210662.png

  • action 内部可以执行异步操作
  • 这样可以精准获取到 mutation 中的状态变更时机
 const actions = {
   incrementAction ({commit},payload) {
     console.log('actions触发');
     setTimeout(() => {
       commit('increment',payload)
     }, 5000);
   }
 }

1668953443827.png

辅助函数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);})
   }
 }

1668954057446.png

  • 可以利用 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)
   }
 }

1668954507448.png

Modules

未完待更... ...