前端学习笔记(15)-Vue3状态管理store及Vuex的使用

1.状态管理

2.用响应式API做简单状态管理

3.Vuex基础

4.Vuex 核心概念

5. Vuex+sessionStorage实现数据存储


1.状态管理

理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了。我们以一个简单的计数器组件为例:

<script setup>
import { ref } from 'vue'

// 状态
const count = ref(0)

// 动作
function increment() {
  count.value++
}
</script>

<!-- 视图 -->
<template>{
    
    { count }}</template>

它是一个独立的单元,由以下几个部分组成:

  • 状态:驱动整个应用的数据源;

  • 视图:对状态的一种声明式映射;

  • 交互:状态根据用户在视图中的输入而作出相应变更的可能方式。

下面是“单向数据流”这一概念的简单图示:

然而,当我们有多个组件共享一个共同的状态时,就没有这么简单了:

  1. 多个视图可能都依赖于同一份状态。

  1. 来自不同视图的交互也可能需要更改同一份状态。

  • 对于情景 1,一个可行的办法是将共享状态“提升”到共同的祖先组件上去,再通过 props 传递下来。然而在深层次的组件树结构中这么做的话,很快就会使得代码变得繁琐冗长。这会导致另一个问题:Prop 逐级透传问题

  • 对于情景 2,我们经常发现自己会直接通过模板引用获取父/子实例,或者通过触发的事件尝试改变和同步多个状态的副本。但这些模式的健壮性都不甚理想,很容易就会导致代码难以维护。

一个更简单直接的解决方案是抽取出组件间的共享状态,放在一个全局单例中来管理。这样我们的组件树就变成了一个大的“视图”,而任何位置上的组件都可以访问其中的状态或触发动作。

参考资料:https://cn.vuejs.org/guide/scaling-up/state-management.html#what-is-state-management

2.用响应式 API 做简单状态管理

如果你有一部分状态需要在多个组件实例间共享,你可以使用 reactive() 来创建一个响应式对象,并将它导入到多个组件中:

// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>
  <button @click="store.increment()">
    From A: {
    
    { store.count }}
  </button>
</template>
<!-- ComponentB.vue -->
<script setup>
import { store } from './store.js'
</script>
<template>
  <button @click="store.increment()">
    From B: {
    
    { store.count }}
  </button>
</template>

为了确保改变状态的逻辑像状态本身一样集中,建议在 store 上定义方法,方法的名称应该要能表达出行动的意图。

3.Vuex基础

以上是一个表示“单向数据流”理念的简单示意

  • 状态,驱动应用的数据源;

  • 视图,以声明方式将状态映射到视图;

  • 操作,响应在视图上的用户输入导致的状态变化。

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

  • 多个视图依赖于同一状态。

  • 来自不同视图的行为需要变更同一状态。

因此,Vuex不把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,代码将会变得更结构化且易维护。

Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

4.Vuex核心概念

4.1 State
4.1.1 store.state

Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 ”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

vuex中的state保存状态数据使用的。在组合式api的调用方法:

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

export default {
  setup () {
    const store = useStore()

    return {
      // 在 computed 函数中访问 state
      count: computed(() => store.state.count),

      // 在 computed 函数中访问 getter
      double: computed(() => store.getters.double)
    }
  }
}

const store = useStore()表示从useStore中获取store对象

然后将获取的值放入computed中,将count放入Vuex中的state中管理。

4.1.2 mapState

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性:

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

export default {
  setup () {
    const store = useStore()
    const storeStateFns = mapState(["counter", "name", "age"]);
    let storeState = {};
    Object.keys(storeStateFns).forEach((fnkey) => {
      // storeStateFns[fnkey]拿到每一个key对应的函数
      // .bind()给fn绑定this 才能传给computed
      const fn = storeStateFns[fnkey].binds({ $store: store });
      // 将函数通过computed函数转为ref
      storeState[fnkey] = computed(fn);
    });
    return {
      ...storeState,
    }
  }
}

上面返回的storeState是一个对象,对象内容如下{counter:fn,name:fn,age:fn},对象中都是函数,可以通过Object.keys(storeState)拿到storeState中的key值。

遍历每一个函数,并通过设置函数的this执行让其指向store

storeState = {}用于保存属性的方法,最终在return中返回,作为计算属性。

在CompositionAPI里使用mapState思路就是

  1. 通过 mapState函数 拿到所传入参数的属性的函数

  1. 通过comouted函数将这些函数转化为ref类型的数据

4.1.3 封装一个hooks

在setup中使用太麻烦,代码太冗余,此时应该封装为useState.js(.ts)文件。

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

export default function(mapper){
    const store = useStore()
    const fns = mapState(mapper);
    const state = {};
    Object.keys(fns).forEach((key) => {
      const fn = fns[key].binds({ $store: store });
      state[key] = computed(fn);
    });
    return state
}

在其他组件中使用:

 import { useState } from '../hooks/useGetters'
 
  export default {
    setup() {
      const storeGetters = useState(["nameInfo", "ageInfo", "heightInfo"]),
        return {
        ...storeGetters
      }    
    }
}

使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。

4.2 Getter

vuex中getter的主要目的是为了数据过滤,得到自己想要的数据。Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

4.2.1 通过属性访问

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值

const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos (state) {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount (state, getters) {
        return getters.doneTodos.length
    }
  }
})

以上代码store.getters.doneTodos获得的值是:[{ id: 1, text: '...', done: true }]

以上代码store.getters.doneTodosCount 获得的值是:1

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

使用方法:

 <script>
 
  import { computed } from "vue"
  import { mapGetters, useStore } from 'vuex'
 
  export default {
    setup() {
      const store = useStore();
 
      const sNameInfo = computed(() => store.getters.nameInfo)
 
      return {
        sNameInfo
      }
    }
  }

使用直接在外层套上computed即可。

4.2.2 通过方法访问

可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

getters:{// ...
    getTodoById:(state)=>(id)=>{
        return state.todos.find(todo=> todo.id === id)
    }
}
store.getters.getTodoById(2)// -> { id: 2, text: '...', done: false }
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
4.2.3 mapGetters辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

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

export default {
  setup () {
    const store = useStore()
    const storeGetterFns = mapGetters(["allBooksPrice"]);
    const storeGetter = {};
    Object.keys(storeGetterFns).forEach((fnkey) => {
      const fn = storeGetterFns[fnkey].binds({ $store: store });     
      storeState[fnkey] = computed(fn);
    });
    return {
      ...storeGetter,
    }
  }
}
4.2.4 封装一个hooks

方法类似于mapState的思路,结合mapGetter与appState封装

useMap.ts

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

export default function(mapper, mapFun){
    const store = useStore()
    const fns = mapFun(mapper);
    const state = {};
    Object.keys(fns).forEach((key) => {
      const fn = fns[key].binds({ $store: store });
      state[key] = computed(fn);
    });
    return state
  }
}

在其他组件中使用:

import {mapGetters, mapState} from "vuex";
import {useMap} from ".hooks"

export default{
    setup(){
        const getters = useMap(["allBooksPrice"],mapGetters);
        const state = useMap(["Price"],mapState);
        return {
            ...getters,
            ...state,
        }
    }
}
4.3 mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。简而言之是是操作state的一些方法,Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

4.3.1 mutations使用
import {createStore} from "vuex"
const store = createStore({
    state(){
        return{
            counter: 1
        }
    },
    mutation:{
        increment(state){
            state.counter++
        }
    }
})

你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:

import {useStore} from 'vuex'
export default{
    setup(){
        cosnt store = useStore()
        increment :() => store.commit("increment")
    }
}
4.3.2 提交载荷(Payload)
mutation:{
    increment(state,n){
        state.count += n
    }
}
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

mutation:{
    increment(state,payload){
        state.count += payload.amount
    }
}
store.commit('increment',{
    amount:10
})
4.3.3 Mutation常量类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.ts
export const SOME_MUTATION = 'SOME_MUTATION'
// store.ts
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能
    // 来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // 修改 state
    }
  }
})

很多时候我们mutation可以单独进行定义,此时可以新建一个文件,里面存放变量名。

// mutation-types.ts
export enum UserMutationTypes {
  SET_USER_INFO = 'SET_USER_INFO',
}
//.ts  
[UserMutationTypes.SET_USER_INFO](state: S,payload: Record<string, any>): void;
4.3.4 mapMutations辅助函数

你可以在组件中使用 store.commit("xxx")提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

export const INCREMENT = "increment"
export const DECREMENT = "decrement"
export const INCREMENT_TEN = "incrementTen"
import {mapMutations } from "vuex"
import {INCREMENT_TEN} from "./store/mutation-types"
export default{
    setup(){
        const mut = mapMutations({
            addOne:"increment",
            subOne:"decrement",
            addTen:INCREMENT_TEN,
        });
        //const mut = mapMutations(["increment","decrement",INCREMENT_TEN]) //数组格式
        return{
            ...mut
        };
    }
}
注意: mutation 必须是同步函数。因为 vue devtool会对每一个 mutations进行快照,并且记录之前的一个值和下一个值,如果是异步的函数,则无法记录。
4.4 Action
在vuex中的 actions选项中保存的是一些异步的方法。在 actions中提交 mutation,而不是直接改变状态。
4.4.1 Action的使用

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。

  • Action 可以包含任意异步操作。

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

我们需要调用 commit 很多次的时候,可以用到参数解构来简化代码:

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
4.4.2 分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

正如4.3 mutations所说, mutation 具有必须同步执行的限制么,而Action 就不受约束。我们可以在 action 内部执行异步操作:

actions: {
  incrementAsync (context) {
    setTimeout(() => {
      context.commit('increment')
    }, 1000)
  }
}

此时在actions中传入的参数为contextcontext是一个和store具有相同的方法和属性。

也可以这么写,参数解构来简化代码:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

在组件中使用 store.dispatch('xxx') 分发 action:

      asyncIncrement: () => store.dispatch('asyncIncrement')
4.4.3 mapActions 辅助函数

如果一个方法或多个方法需要在多个页面和组件中使用,那么,可以使用mapActions。想要调用多少个 action 就需要调用多少次 dispatch() ,而使用 mapActions 的话只需要往 mapActions 中传入与 action 同名的函数,然后调用这些函数即可触发对应的action

<script>
import {mapActions} from "vuex"
export default{
    setup(){
        const actions = mapActions({
            incrementAction:"incrementAction",
            incrementBySelf:"incrementBySelf",
            decrementBySelf:"decrementBySelf",
        });
        return{
            ...actions,
        };
    },
};
</script>

<template>
    <div>
        <h1>{
    
    {$store.state.counter}}</h1>
        <button @click="incrementAction">+1</button>
        <button @click="incrementBySelf({count: 10})">+10</button>
        <button @click="decrementBySelf({count: 10])">-10</button>
    </div>
</template>
4.5 module

Vuex集中式存储管理应用的所有组件的状态,放在store中,当项目达到一定的规模,那么这个store就会变得十分的复杂。这时候就需要将这样一个庞大的store进行分类处理,也就是将将store分割成一个个module(模块),便于日后的修改和管理。这与我们生活中对仓库中的物品进行分类是一样的,例如将电子类产品放在a货架上,将日常生活产品放在b货架上,这样的货架就对应了一个个module。每个module拥有自己的state、mutations、actions以及getters。

4.5.1 module的基本使用
import {createStore} from "vuex"
import homeModule from "./modules/home"
import aboutModule from "./modules/about"

const store = createStore({
    state(): => ({
        return {
            counter:0,
        }
    }),
    modules:{
        home:homeModule,
        about:aboutModule
    }
})
store.state.home // -> moduleHome 的状态
store.state.about // -> moduleAbout 的状态

export default store
//homeModule.ts
export default{
    state: ()=>({
        return{
            homeCounter:0
        }
    }),
    getters:{
    
    },
    mutation:{
    
    },
    actions:{
    
    },
}
//aboutModule.ts
export default{
    state:()=> ({
        return{
            aboutCounter:0
        }
    }),
    getters:{
    
    },
    mutation:{
    
    },
    actions:{
    
    },
}
4.5.2 modules的局部状态参数

官网的例子:https://vuex.vuejs.org/zh/guide/modules.html#

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 的状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象,也就是本模块自己的state,mutation 另一个参数是调用该函数的参数(payload)。以下代码中的mutation和getter中的参数state都是muduleA自己的state(局部状态),所以可以直接通过state.count访问到count的状态。

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

同样,对于模块内部的 action,存在两个参数,一个是context,另一个是传入的参数payload。

第一个参数context是一个对象,其中包含了commit,dispatch,getters,rootGetters, state,rootState。

局部状态通过 context.state 暴露出来,根节点状态(也就是整个store最外层的状态)为

context.rootState。以下代码中的state.count和rootState.count分别就是moduleA自己的状态中的

count和外部store中的状态的count。

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来,也就是在getter中引入根节点的状态,是通过getter的第三个参数来获取。

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}
4.5.3 module的命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的。这时候会存在弊端:

  • 弊端1:不同模块中有相同命名的mutations、actions时,不同模块对同一 mutation 或 action 作出响应。

  • 弊端2:当一个项目中store分了很多模块的时候,在使用辅助函数mapState、mapGetters、mapMutations、mapActions时,很难查询,引用的state、getters、mutations、actions来自于哪个模块,不便于后期维护。

因此可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。你的模块会具有更高的封装度和复用性。

const store = createStore({
  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']
          }
        }
      }
    }
  }
})
启用了命名空间的 getter 和 action 会收到局部化的 getter,dispatch 和 commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

5.Vuex+ sessionStorage实现数据储存

当一个项目逻辑层多,涉及到页面间的传参的参数会很多,页面之间跳转频繁。由于项目变得越来越复杂,单单使用Vuex会出现一些刷新时参数丢失的问题,面对所有页面都可能需要使用到的,比如说用户的登录状态,下拉框值,可以将整体的传参方式改成vuex+sessionStorage。页面涉及多层面包屑,但是不想使用keep-alive时、页面之间有很多参数是相同的,需要从一个页面带到另一个页面也可以使用Vuex+ sessionStorage实现数据储存。

5.1 Vuex数据状态持久化的使用场景

1、购物车

比如你把商品加入购物车后,没有保存到后台的情况下,前端来存,就可以通过这种方式。

2、会话状态

授权登录后,token就可以用Vuex+sessionStorage来存储。

3、一些不会经常改变的数据

比如城市列表等(当前也要留下可以更新的入口,比如版本号)

5.2一个栗子:
  1. 使用this.$store.commit()将需要缓存的数据存储到Vue store

this.$store.commit('setRemark',row.remark)
  1. 在store中新建一个js文件用于接收和修改this.$store.commit()提交上来的数据

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
    // 设置默认值
    remark:''
}
const mutations = {
    setRemark(state,remark){
        state.remark = remark
        sessionStorage.setItem('remark',remark)
      },
}
const actions = {
  
}
const getters = {
     remark:(state) => sessionStorage.getItem('remark'),
}
  1. 在需要使用的界面取值

sessionStorage.getItem['remark']
  1. 更新数据

sessionStorage.setItem('remark',remark)

猜你喜欢

转载自blog.csdn.net/JiangZhengyang7/article/details/128820796