[TOC]

状态管理

什么是状态管理

image.png
为啥说是状态,对于我们来说是数据,但是对于机器来说只是变量某个时刻的状态,所以称为状态。

复杂的状态管理

image.png

Vuex 的状态管理

管理不断变化的state本身是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;

因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?

  • 在这种模式下,我们的组件树构成了一个巨大的 “视图View”;
  • 不管在树的哪个位置,任何组件都能获取状态或者触发行为;
  • 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;

这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想):
image.png
vuex 的设计整体保持了 数据仓库 —— 视图 —— 行为 这一单向流动过程。
这里行为分成了两部分:Mutations 和 Actions,主要是为同步行为和异步行为而分,mutations 中不能进行异步操作。

为什么设计将行为进行区分?
主要原因是当我们使用 devtools 时,devtools 可以帮助我们捕捉 mutations 的快照。但如果是异步操作,那么devtools 将不能很好的追踪这个操作什么时候会被完成。会导致devtools 记录的数据没有与页面显示的数据保持一致。所以为了处理异步操作就多添加一层 actions 专门处理异步操作。

Vuex 的安装

我们这里使用的是 vuex4.x,vuex3.x 还是默认版本的时候,安装的时候需要添加 next 获取默认版本的下一个版本。

  • npm install vuex@next

如果不知道当前默认版本是哪个,可以通过 npm 查看远程仓库的默认版本

  • npm view 包名

现在默认版本就是 4.0,可直接下载

  • npm i vuex

    Store

    每一个Vuex应用的核心就是store(仓库):

  • store本质上是一个容器,它包含着你的应用中大部分的状态(state);

Vuex和单纯的全局对象有什么区别呢?

  • 第一:Vuex的状态存储是响应式的
    • 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
  • 第二:你不能直接改变store中的状态
    • 改变store中的状态的唯一途径就显示提交 (commit) mutation;
    • 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;

使用步骤:

  • 创建Store对象;
  • 在app中通过插件安装; ```javascript import { createStore } from “vuex”

// 创建 store 仓库对象 const store = createStore({ state() { return { rootCounter: 100 } }, mutations: { increment(state) { state.rootCounter++ } } });

export default store;

```javascript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 注册 store 插件,另外use 返回的是 app 对象,所以可以链式使用插件
createApp(App).use(router).use(store).mount('#app')

组件中使用store:

<template>
  <div>
    <h1>HelloWorld</h1>
    <!-- 模板中获取 store 仓库中的数据 -->
    <h2> {{ $store.state.rootCounter }}</h2>
    <h2> {{ counter }} </h2>
    <button @click="increase">+1</button>
  </div>
</template>

<script>
  export default {
    methods: {
      increase() {
        // 通过行为修改参数数据
        this.$store.commit('increment')
      }
    },
    computed: {
      // options api 获取数据仓库中的数据,计算属性也封装了过长的表达式
      counter() {
        return this.$store.state.rootCounter
      }
    }
  }
</script>

单一状态树

image.png

组件获取状态

上面一句有组件中获取状态的方式,但是表达式太长了很繁琐。虽然用计算属性转换了一下,但是大量的仓库数据计算属性会和其他逻辑的计算属性杂糅在一起,一大半都是这种转换数据的计算属性,喧宾夺主了。

  • 因此建议使用 vuex 的 mapState辅助函数,自动完成了 state 到计算属性的映射。

mapState 可以将 state 以对象或者数组的方式接收,并返回一个对象,对象中属性就是映射好的同名计算属性,并且属性是函数类型。
所以我们一般使用展开运算符来将映射好的计算属性一个一个取出,注册到 computed 对象中。

<template>
  <div>
    <h1>HelloWorld</h1>
    <!-- 通过计算属性 rootCounter 获取 state 中的数据 -->
    <h2> {{ rootCounter }} </h2>
    <!-- 自定义了计算属性名 -->
    <h2>{{hhh}}</h2>
  </div>
</template>

<script>
  import { mapState } from "vuex"
  export default {
    computed: {
      // 展开运算符取出隐射好的同名计算属性
      // 返回值是{ rootCounter: function }展开取出来放到 computed:{ }中正好注册为计算属性
      ...mapState(['rootCounter']),
      // 对象的形式,可以自定义数据的名字
      ...mapState({
        hhh: state => state.rootCounter // 通过函数取出仓库中的数据
      })
    }
  }
</script>

在 setup 中使用 mapState

在 setup 中和 vue-router 一样,要拿到插件提供的全局对象,需要通过 hook。拿到 store 对象需要通过 useStore hook。
setup 中单个数据转成计算属性和在 computed 选项中一样问题不大,问题就在于如果有大量仓库数据,setup 中也会写的很繁琐,需要使用 mapSate ,但是默认情况下,Vuex 并没有提供非常方便的使用mapState的方式,所以我们可以自己进行了一个函数的封装:

import { useStore } from "vuex"
import { computed } from "@vue/reactivity"
export default {
  setup() {
    const store = useStore()
    // 单个仓库数据
    const count = computed( () => store.state.rootCounter)

    // 封装函数对多个数据进行批量处理
    function myMapState(iterableObj) {
      const store = useStore()
      const obj = {} // 以一个对象做载体保存计算属性 ref 对象
      for (const item of iterableObj) {
        // 注意不要写成 .item,要写成 [item] 做计算
        obj[item] = computed( () => store.state[item]) 
      }
      return obj
    }
    // 解构取出所有计算属性 ref 
    return { count, ...myMapState(['name', 'age', 'rootCounter'])}
  }
}

为了方便后续使用,可以封装为一个 hook。

import { useStore } from 'vuex'
import { computed } from '@vue/reactivity'

const useMapState = iterableObj => {
  const store = useStore()
  const obj = {} // 以一个对象做载体保存计算属性 ref 对象
  for (const item of iterableObj) {
    // 注意不要写成 .item,要写成 [item] 做计算
    obj[item] = computed(() => store.state[item]) 
  }
  return obj
}
export default useMapState

也可以通过 mapState 进行封装:

import { computed } from 'vue'
import { mapState, useStore } from 'vuex'

export function useState(mapper) {
  // 拿到store独享
  const store = useStore()

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapState(mapper)

  // 对数据进行转换
  const storeState = {}
  Object.keys(storeStateFns).forEach(fnKey => {
    // mapState 实际也是通过 this.$store.state 拿到数据的,所以我们要手动绑定 this
    const fn = storeStateFns[fnKey].bind({$store: store})
    storeState[fnKey] = computed(fn)
  })

  return storeState
}

getters

某些属性我们可能需要经过变化后来使用,这个时候可以使用 getters,和计算属性类似。
getters 的属性都是函数,除了可以接收 state 对象还可以接收 getters 做参数,通过 getters 就可以调用其他的 getter 属性。

import { createStore } from "vuex"

const store = createStore({
  state() {
    return {
      name: 'zs',
      age: 18,
      books: [
        { aaa: '入门放弃', price: 100 },
        { bbb: 'csapp', price: 99 }
      ]
    }
  },
  getters: {
    tatolPrice(state, getters) {
      console.log(getters.person); // 调用其他的 getter
      return state.books.filter(book => {
        return book.price < 100
      })
    },
    person(state) {
      return state.name + state.age
    }
  }
})

export default store;

获取 getters 中的数据,也是通过 store 对象

  • $store.getters
  • useStore().getters

    getters 的返回函数

    getters 和计算属性一样,使用的时候好像是当属性使用,但属性在 getters 中是函数形式,我们可以让 getter 返回一个函数,那么使用的时候就可以像是调用函数,并且传递参数。

    getters: {
    totalPrice(state) {
      return price => {
        const arr = state.books.filter(book => {
          return book.price == price
        })
        return arr
      }
    },
    person(state) {
      return state.name + state.age
    }
    }
    
    setup() {
    // 平常使用 getters 中的 getter
    console.log( useStore().getters.person);
    // 当 getters 中的 getter 为返回函数的形式,可以传递参数了
    console.log( useStore().getters.totalPrice(100));
    }
    

    mapGetters 辅助函数

    我们一般把仓库数据放到计算属性 computed 中转成 ref 对象,getters 也不例外,和 state 一样也会在 computed 中出现代码杂糅的问题。
    我们需要使用mapGetters函数来简化 ```html

    ```html
    <template>
    
      <h2>{{totalPrice(100)}}</h2>
      <h2>{{person}}</h2>
    
    </template>
    
    <script>
      import { mapGetters } from "vuex"
    
      export default {
        setup() {
          const store = useStore()
    
          const person = computed( () => store.getters.person)
          const totalPrice = computed( () => store.getters.totalPrice)
    
          return { person, totalPrice }
        }
      }
    </script>
    

    setup 中对于大量的 getter 依然需要自己封装函数。但是 getters 和 state 的代码重复度很高,所以可以考虑,让 useMapState 也适应 getters。

    import { useStore } from 'vuex'
    import { computed } from '@vue/reactivity'
    
    const useMapper = (iterableObj, mapper = 'state') => {
      const store = useStore()
      const obj = {} // 以一个对象做载体保存计算属性 ref 对象
      for (const item of iterableObj) {
    
        if (mapper == 'getters') {
          obj[item] = computed(() => store.getters[item])
        } else {
          obj[item] = computed(() => store.state[item])
        } 
    
      }
      return obj
    }
    
    export default useMapper
    

    Mutation

    更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。简单使用已经在 store 中演示。

    mutation 中函数不止接收 state 一个参数,第二个参数 payload 负载,表示调用时传入的参数,参数可为普通值,也可为对象。
    commit 提交改变时传入参数,并且有两种编码风格。

    state() {
      return {
        rootCounter: 100
      }
    },
      mutations: {
        increment(state, payload) {
          state.rootCounter += payload.count
        }
      }
    
    // app.vue
    mothods: {
      // 第一种风格
      add() { this.$store.commit('increment', {count: 100}), // 提交参数
      // 第二种风格,直接写在一个对象里
      add1() {
        this.$store.commit({
          type: 'increment', // type表示 mutation 中的函数
          count: 100
        })
      }
    }
    

    Mutation 常量类型

    有时候大家为避免 mutation 中函数名写错,会采用常量的方式进行维护函数名。
    定义常量:mutation-type.js
    image.png
    定义mutation
    image.png
    提交mutation
    image.png

    mapMutations 辅助函数

    我们也可以借助于辅助函数,帮助我们快速映射到对应的方法中:

    import { useStore, mapMutations } from 'vuex'
    
    export default {
      // ...
      methods: {
        ...mapMutations([
          'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
    
          // `mapMutations` 也支持载荷:
          'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
        ]),
        ...mapMutations({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
        })
      },
      setup() {
        const store = useStore()
        ...mapMutations(['increment', 'incrementBy'])
      }
    }
    

    mutation 重要原则

    一条重要的原则就是要记住 mutation 必须是同步函数

    • 这是因为devtool工具会记录mutation的日记;
    • 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
    • 但是在mutation中执行异步操作,就无法追踪到数据的变化;
    • 所以Vuex的重要原则中要求 mutation必须是同步函数;

      actions

      异步请求的数据我们可以在组件中获取,但是如果涉及到共享,就可以让 vuex 来管理这些异步请求的数据。

    actions 是从 mutation 中特意分出来的一层,处理异步的数据,所以 actions 作为 mutation 的上游,并不会直接变更数据,而是 commit 提交到 mutation 中进行变更

    • mutation 中的变更是由 store 对象进行提交,而 actions 中是 **context** 对象。

    context 是一个和 store 实例均有相同方法和属性的context对象;

    • 所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters;
    • 但是为什么它不是 store 对象呢?这个和 Modules 有关。 ```javascript import { createStore } from ‘vuex’

    const store = createStore({ state() { return { rootCounter: 100, } }, mutations: { increment(state, mutationsPayload) { state.rootCounter += mutationsPayload } }, actions: { incrementAction(context, actionsPayload) { console.log(actionsPayload); // action 也能接受参数 // 因为需要异步获取数据,所以提交更改到 mutation 是异步的 setTimeout(() => { context.commit(‘increment’, 10) }, 1000); } } })

    export default store

    ```html
    <template>
      <h2>{{$store.state.rootCounter}}</h2>
      <button @click="add"> +1 </button>
      <button @click="add1"> +1 </button>
      <br>
      <button @click="incrementAction">+1</button>
    </template>
    
    <script>
      import { useStore } from "vuex"
      export default {
        methods: {
          add() {
            // options api中分发 actions 并且提供参数,如果是 commit 就是调用 mutation中的函数了
            this.$store.dispatch('incrementAction', 123)
          },
          add1() {
            // action 分发也支持对象的编码风格,参数传过去也是对象的形式
            this.$store.dispatch({
              type: 'incrementAction',
              count: 456
            })
          }
        },
        setup() {
          const store = useStore()
          // composition api 分发 action
          const incrementAction = () => store.dispatch('incrementAction', 111)
    
          return { incrementAction }
        }
      }
    </script>
    

    context 对象

    context 对象中不止 commit 函数,其中还有 dispatch 等函数,dispatch 是用来分发 action 的,在 actions 中使用 dispatch 自然是用来分发其他 action,套娃分发 action。
    对于 context 我们可以直接解构使用,而不用引入整个 context 对象。

    mutations: {
      increment(state, mutationsPayload) {
        state.rootCounter += mutationsPayload
      }
    },
    actions: {
      incrementAction({commit, dispatch}, actionPayload) {
        console.log(actionPayload);
        dispatch('text') // 调用其他 action
        setTimeout(() => {
          commit('increment', 10) // 直接使用 commit
        }, 1000);
      },
      text(context) {
        console.log(context);
      }
    }
    

    mapActions 辅助函数

    methods: {
      ...mapActions(['incrementAction', 'text']) // 数组形式
    },
    setup() {  
       // return { ...mapActions(['incrementAction', 'text']) }
       return {
         ...mapActions({ // 对象形式
           increment: 'incrementAction', 
           tex: 'text'
         })
       }
     }
    

    actions 的异步操作

    模板中进行 action 分发的时候,因为 action 是异步的,那在模板中怎么知道 action 异步结束了呢?
    可以让 action 返回一个 promise,异步操作都在这个 promise 中完成

     actions: {
       incrementAction( {commit} ) {
         return new Promise((resolve) => {
           setTimeout(() => {
             commit('increment', 10) // 直接使用 commit
             resolve('请求成功')
           }, 1000);
         })
       }
    }
    
    setup() {
      const store = useStore()
    
      const incrementAction = () => store.dispatch('incrementAction').then( res => {
        console.log(res); // 请求成功
      })
    
      return { incrementAction }
    }
    

    module

    什么是Module?
    由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可
    能变得相当臃肿;
    为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块;

    • 对于模块内部的 mutation 和 getter,接收的第一个参数 state 是当前模块中的局部状态对象 state
    import { createStore } from 'vuex'
    import homeModule from './module/home' // 导入其他 store 模块
    
    const store = createStore({
      state() {
        return {
          rootCounter: 100
        }
      },
      mutations: {
        increment(state, mutationsPayload) {
          // state.rootCounter += mutationsPayload
          state.rootCounter ++
        }
      },
      modules: {
        home: homeModule // 注册模块
      }
    })
    
    export default store
    
    const homeModule = {
      state() {
        return {
          homeCounter: 10
        }
      },
      getters: { },
      mutations: {
        increment(state) { // 和主 store 对象中的 mutation 函数同名
          state.homeCounter++
        }
      },
      actions: { }
    }
    
    export default homeModule
    
    <template>
      <h2>{{$store.state.rootCounter}}</h2>
    
      <!-- 通过模块名获取模块中的state -->
      <h2>{{$store.state.home.homeCounter}}</h2>
    
      <button @click="increment">+1</button>
    
    </template>
    
    <script>
      import { useStore } from "vuex"
      export default {
        setup() {
          const store = useStore()
    
          const increment = () => store.commit('increment')
    
          return { increment }
        }
      }
    </script>
    

    module 的命名空间

    现在存在一个问题,主 store 对象和模块中的 mutation、getters、actions 是会混在一起的。默认情况下,模块内部的 mutation、getters、actions 注册在全局的命名空间。

    • 比如 home 模块中有 gettes 为 homeincrease()。在模块中提交的方式为$store.gettes.homeincrease。因为混在一起,所以不用特别指定就能调用,但是这就导致除了命名压根看不出 homeincrease 这个 getter 是来自 home 模块。
    • 如果和上面示例代码一样,mutation 中函数名是一样的,那么 commit 的时候,主模块和子模块 home 中的 increment 都会执行。

    为了解决这种模块不清的问题,可以开启各个模块自己的命令空间,添加 namespaced: true 的方式使其成为带命名空间的模块。

    • 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名; ```javascript const homeModule = { namespaced: true, // 开启命名空间 state() { return {
      homeCounter: 10
      
      } }, getters: { // 子模块中的 getter 有 4 个参数 doubleHomeCounter(state, getters, rootState, rootGetters) {
      return state.homeCounter * 2
      
      } }, mutations: { increment(state) { // 和主 store 对象中的 mutation 函数同名
      state.homeCounter++
      
      } }, actions: { } }

    export default homeModule

    ```html
    <template>
      <!-- 开启命名空间模块后,已经不能直接从 getters 中获取到模块中的 getter 了 -->
      <h2> {{ $store.getters.doubleHomeCounter }}</h2>
      <!-- 需要指定模块名,用方括号计算出 getter 的位置 -->
      <h2> {{ $store.getters['home/doubleHomeCounter'] }}</h2>
    
      <button @click="increment">+1</button>
    
    </template>
    
    <script>
      import { useStore } from "vuex"
      export default {
        setup() {
          const store = useStore()
          // 指定提交 home 模块中 mutation,此时主模块中的同名 increment 不会执行
          const incrementAction = () => store.commit('home/increment')       
          // const increment = () => store.commit('increment')
    
          return { increment }
        }
      }
    </script>
    

    子模块对根模块的访问

    子模块中 actions 中提交 action 默认是提交到本模块中的 mutation,如果想要提交到根模块,则可以在 commit 第三个参数中设置:{root: true}

    actions: {
      // 6 个参数
      incrementAction({commit, dispatch, state, rootState, getters, rootGetters}) {
        commit("increment")
        commit("increment", null, {root: true}) // 提交到根模块的 increment
      }
    }
    
    // 模板中分发 action 时也可以指定分发到根模块
    // dispatch
    dispatch("incrementAction", null, {root: true})
    

    子模块怎么获取根模块中的 state 和 getter 呢?

    • getters 函数中多了两个参数,rootStaterootGetters代表了根模块中的 state 和 getter
    • actions 中 context 对象和 state 很像,不同点就是 context 对象多了也多了两个参数rootStaterootGetters,就是来访问根模块数据的。

      module 的辅助函数

      之前对 state、getters、mutation、actions 都有对应的辅助函数 mapState、mapGetters、mapMutations、mapActions。
      辅助函数默认接收的数组或对象都是针对的根模块进行匹配,怎么匹配拥有命名空间的子模块呢?

    有三种写法:

    1. 对象参数方式下指定子模块
    2. 辅助函数中第一个参数可以指定子模块
    3. 通过createNamespacedHelpers函数对辅助函数进行修改,为子模块生成一套对应的辅助函数

      • 这样使用辅助函数就不用一个一个指定模块了,和最开始一样正常使用就行 ```javascript computed: { // 1.写法一: …mapState({ homeCounter: state => state.home.homeCounter }), …mapGetters({ doubleHomeCounter: “home/doubleHomeCounter” })

      // 2.写法二: …mapState(“home”, [“homeCounter”]), …mapGetters(“home”, [“doubleHomeCounter”])

    }, methods: { // 1.写法一: …mapMutations({ increment: “home/increment” }), …mapActions({ incrementAction: “home/incrementAction” })

    // 2.写法二 …mapMutations(“home”, [“increment”]), …mapActions(“home”, [“incrementAction”]), }

    ```javascript
    import { createNamespacedHelpers } from "vuex";
    // 解构获取修改后的辅助函数
    const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("home")
    
    computed: {  
    
      ...mapState(["homeCounter"]),
      ...mapGetters(["doubleHomeCounter"])
    },
    methods: {
    
      ...mapMutations(["increment"]),
      ...mapActions(["incrementAction"]),
    }
    

    composition api

    setup() {
      // state 和 getter 这样是不行的,因为映射返回的是函数,在模板上就会展示出一个函数
      // 所以之前我们都是放到 computed 中,转成 ref 对象,则可在模块中直接展示值。
      const state = mapState(["rootCounter"])
      const getters = mapGetters(["doubleHomeCounter"])
    
      // 这两个没问题,因为本来就是函数
      const mutations = mapMutations(["increment"])
      const actions = mapActions(["incrementAction"])
    
      return {
        ...state,
        ...getters,
        ...mutations,
        ...actions
      }
    }
    

    对 useState 和 useGetters 修改

    为了解决上面的问题,我们也能使用自己封装的 hook 来处理 state 和 getters。先前的 hook 没有对模块进行处理,所以需要修改一下。

    import { mapState, createNamespacedHelpers } from 'vuex'
    import { useMapper } from './useMapper'
    
    export function useState(moduleName, mapper) {
      let mapperFn = mapState
      if (typeof moduleName === 'string' && moduleName.length > 0) {
        mapperFn = createNamespacedHelpers(moduleName).mapState
      } else {
        mapper = moduleName
      }
    
      return useMapper(mapper, mapperFn)
    }
    
    import { mapGetters, createNamespacedHelpers } from 'vuex'
    import { useMapper } from './useMapper'
    
    export function useGetters(moduleName, mapper) {
      let mapperFn = mapGetters
      if (typeof moduleName === 'string' && moduleName.length > 0) {
        mapperFn = createNamespacedHelpers(moduleName).mapGetters
      } else {
        mapper = moduleName
      }
    
      return useMapper(mapper, mapperFn)
    }
    
    const getters = useGetters("home", ["doubleHomeCounter"]) // 使用自己封装的 hook