Vuex的状态管理

vuex介绍

  • 什么是状态管理?

    应用程序的各种数据保存到某个位置进行管理

image.png

​ state:数据 view:最终模块渲染成DOM actions:修改state的行为事件

  • 复杂的状态管理

    多个组件共享状态

  • Vuex的状态管理

    将组件的内部状态抽离出来

    image.png
  • Vuex使用单一状态树

    SSOT:Single Source of Truth,单一数据源

    每个应用仅仅包含一个store实例

安装

npm install vuex@next

使用vuex4.x,需要添加next指定版本

使用

创建store(仓库)

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

    Vuex的状态存储是响应式的

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

    改变store中的状态的唯一途径就是提交(commit) mutation

    这样方便我们跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态

  • 具体步骤:

    1. store/index
    import { createStore } from 'vuex'

    // 创建一个新的store实例
    const store = createStore({
    state() {
    return {
    count: 0 // 数据
    }
    },
    mutations: { // 方法
    increment(state) {
    state.counter++
    },
    decrement(state) {
    state.counter--
    }
    },
    })

    export default store
    1. 在全局将 store 实例作为插件安装
    import store from './store/index'
    createApp(App).use(store).mount('#app')

组件中使用store

state

使用store中的state数据

  • 在模板中使用

    <h2>{{$store.state.count}}</h2>
  • 在options api 中使用

    computed: {
    counter() {
    return this.$store.state.counter
    }
    }
  • 在setup中使用

    setup() {
    const store = useStore()
    const counter = store.state.counter
    }

mapState

如果需要拿state中的多个数据,在可以借助辅助函数 mapState,

  • setup中使用mapState

    默认情况下,Vuex没有提供非常方便的的使用mapState的方式,所以我们进行了一个函数的封装

    <template>
    <div>
    <h2>Home: {{sCounter}}</h2>
    <h2>Home: {{counter}}</h2>
    <h2>Home: {{name}}</h2>
    <h2>Home: {{age}}</h2>
    <h2>Home: {{height}}</h2>
    </div>
    </template>
    <script>
    import { mapState, useStore } from "vuex"
    import { computed } from 'vue'
    export default {
    // options api使用mapState
    computed: {
    // fullName: function() { return xxx},
    ...mapState(["counter", "name"])
    },
    setup() {
    const store = useStore()
    const sCounter = computed(() => store.state.counter)

    // 实际上放入mapState这里拿到的是这样形式的一个个函数
    // {counter: function(), name: function() ...}
    const storeStateFns = mapState(["counter", "name", "age", "height"])

    // 封装一个函数转化一下,主要思想是把这一个个的函数放到computed里面
    // 因为computed就是传入一个函数,然后会给我们返回一个ref
    // {counter: ref, age: ref, ...}
    const storeState = {}
    Object.keys(storeStateFns).forEach(fnKey => {
    // 因为内部的computed取数据的时候是通过this.$store...
    // 但我们这里的fn没有this, undefined.$store 是错的
    // 用bind给每个函数绑定this为一个对象,里面需要有$store这个属性
    // {$store: store}
    const fn = storeStateFns[fnKey].bind({ $store: store})
    // 然后把函数一个方法computed, 以键值对的方式存储起来
    storeState[fnKey] = computed(fn)
    })


    return {
    sCounter,
    // 最终在这里用展开运算符展开
    ...storeState
    }
    }
    }
    </script>

  • 把函数的封装抽离到 hooks/useState.js

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

    export function useState(mapper) {
    const store = useStore()

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

    // 对数据进行转换
    const storeState = {}
    Object.keys(storeStateFns).forEach(fnKey => {
    const fn = storeStateFns[fnKey].bind({ $store: store })
    storeState[fnKey] = computed(fn)
    })

    return storeState
    }

    我们在组件使用就会简便很多

    import { useState } from '../hooks/useState'

    export default {
    setup() {
    const storeState = useState(["counter", "name", "age", "height"])

    // 当然也可以是对象形式(想要重命名的时候使用)
    const storeState2 = useState({
    sCounter: state => state.counter,
    sName: state => state.name
    })
    return {
    ...storeState,
    ...storeState2
    }
    }
    }

getters

  • getters 的基本使用

    某些属性可能需要经过变化后才使用,(就像store中的计算属性)

    image.png
  • getters 第二个参数

    getters可以接收第2个参数getters,使用getters本身的属性

    getters: {
    totalPrice(state, getters) {
    return state.books.reduce((pre, cur) => {
    return pre + cur.count * cur.price
    }, 0) + " " + getters.myName
    },
    myName(state) {
    return state.name
    }
    }
  • getters 的返回函数

    getters中的函数本身,可以返回一个函数,那么在使用的地方相当于可以调用这个函数

    totalPrice(state) {
    return (price) => {
    let totalPrice = 0
    for (const book of state.books) {
    if (book.price < price) continue
    totalPrice += book.count * book.price
    }
    return totalPrice
    }
    },

mapGetters

与mapState类似

  • 在setup中使用mapGetters

    封装好的 /useGetters

    import { computed } from "vue";
    import { useStore, mapGetters } from "vuex";

    export function useGetters(mapper) {
    const store = useStore()

    const stateFns = mapGetters(mapper)

    const state = {}
    Object.keys(stateFns).forEach(fnKey => {
    state[fnKey] = computed(stateFns[fnKey].bind({ $store: store }))
    })

    return state
    }

    使用:

    setup() {
    const storeGetters = useGetters(["nameInfo", "ageInfo", "heightInfo"])
    return {
    ...storeGetters
    }
    }

封装useMapper

我们发现前面 useState,useGetters的逻辑大部分相同,所以我们可以封装一个新的函数,根据使用的时候的 mapState还是mapGetters 来调用函数

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

// mapFn:使用的mapXXX
export function useMapper(mapper, mapFn) {
const store = useStore()

const storeStateFns = mapFn(mapper)

const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({ $store: store })
storeState[fnKey] = computed(fn)
})
return storeState
}
image.png

Mutations

更改 Vuex 的store中的状态的唯一方法是提交mutation

  • mutations 基本使用

    在store中定义方法

    mutations: {
    increment(state) {
    state.counter++
    },
    decrement(state) {
    state.counter--
    }
    }

    在setup中commit事件,提交的是mutations中的方法

    import { useStore } from "vuex"
    setup() {
    const store = useStore()
    store.commit("increment", xxx) // 可以传参数
    }
  • mutation携带数据

    很多时候提交mutation会携带一些数据,在mutation中第二个参数可以接收

    mutations: {
    addNumber(state, payload) {
    state.counter += payload
    }
    }

    payload也可以是对象类型

    提交的时候:可以用type,指定提交的方法名

    $store.commit({
    type: "addNumber",
    count: 100
    })
    mutations: {
    addNumber(state, payload) {
    state.counter += payload.count
    }
    }
  • mutation 常量类型

    主要是预防粗心的时候,commit的方法名字和mutation定义的方法名不一致

    • 定义常量

      // mutation-types.js
      // 定义常量
      export const INCREMENT_N = "INCREMENT_N"
    • store中使用

      import { INCREMENT_N } from '../store/mutation-types'

      const store = createStore({
      state() {...},
      mutations: {
      [INCREMENT_N](state, payload) {
      state.counter += payload
      }
      }
    • 组件中提交事件的时候使用

      import { INCREMENT_N } from '../store/mutation-types'
      export default {
      setup() {
      ...
      store.commit(INCREMENT_N, 10)
      }
      }
  • mutation重要原则

    mutation 必须是同步函数

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

mapMutations

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

const mutations = mapMutations(["increment", "decrement"])
const mutations2 = mapMutations({
addNumber: ADD_NUMBER
})

actions

  • action提交mutation

    • actions 类似于mutations,但是action提交的是mutation,而不是直接变更状态

    • actions 可以包含任意异步操作

    • actions有一个很重要的参数 context,里面有很多属性,我们用的时候除了context.xxx使用,也可以解构出来使用

    • 从context获取commit方法来提交一个mutation

  • actions的分发操作(触发actions中的方法)

    在组件中使用store上的dispatch进行分发,并且可以传递参数

    // store.js
    mutations: {
    increment(state) {
    state.counter++
    }
    },
    actions: {
    // 1. 可以接收参数
    incrementAction(context, payload) {
    console.log(payload) // payload为接收到的参数
    // 模拟异步:1s之后再提交事件
    setTimeout(() => {
    context.commit("increment") // 提交mutation
    })
    },
    // 2. context 的属性
    decrementAction({ commit, dispatch, state, rootState, getters, rootGetters }) {
    commit("decrement")
    }
    }
    // 组件的setup中
    const increment = () => {
    // 分发actions,并携带参数
    store.dispatch("incrementAction", {count: 100})
    }

    另外,也可以以对象的形式进行分发

    // 组件的setup中
    const increment = () => {
    store.dispatch({
    type: "incrementAction",
    count: 100
    })
    }

mapActions

actions 也有对应的辅助函数 mapActions

// 这样用不行..
const actions = mapActions["incrementAction", "decrementAction"]

// 对象写法(重命名)
const actions2 = mapActions({
add: "incrementAction",
sub: "decrementAction"
})

actions的异步操作

actions很多时候是异步的,那么当我们组件派发actions的时候,我们也想收到结果,是请求成功了还是失败了,这时候我们可以在actions对应方法中返回 Promise,并对成功失败做处理

actions: {
getHomeMultidata(context) {
return new Promise((resolve, reject) => {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
console.log(res);
context.commit("addBannerData", res.data.data.banner.list)
resolve("okok")
}).catch(err => {
reject(err)
})
})
}
}
onMounted(() => {
const promise = store.dispatch("getHomeMultidata")
promise.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
})

module

module的基本使用

什么是module?

  • 由于使用单一状态树,应用的所有状态都集中到一个比较大的对象,当应用变得复杂时,store对象就变得相当臃肿,不利于管理
  • 所以Vuex允许我们将store分模块
  • 每个模块拥有自己的state,mutation,action,getter,甚至是嵌套子模块

使用:

image.png
  • 模块内部的mutation和getter的第一个参数state是模块的局部状态对象

module的命名空间

默认情况下,模块内部的action,mutation仍然是注册在全局的命名空间中的

如果希望模块具有更高的封装度和复用性,可以在模块中添加namespaced: true

使模块更独立

之后,当模块被注册后它的所有getter、action及mutation都会自动根据模块注册的路径调整命名

image.png

module修改或派发根组件

{root: true}

actions: {
incrementAction({commit, dispatch,state}) {
commit("rootIncrement", null, {root: true})
dispatch("rootIncrementAction", null, {root: true})
}
}

module的辅助函数

写法一:通过完整的模块空间名来查找(不是很推荐使用)

computed: {
...mapState({
homeCounter: state => state.home.homeCounter,
}),
...mapGetters({
doubleHomeCounter: "home/doubleHomeCounter",
}),
},

写法二:第一个参数写模块名,第二个参数写属性

computed: {
...mapState("home", ["homeCounter"])
}

写法三:createNamespacedHelpers辅助函数

创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数

import { createNamespacedHelpers } from 'vuex'
const { mapState, mapGetters} = createNamespacedHelpers("home")

...mapState(["homeCounter"])

setup中使用

修改之前的 hooks,useStateuseGetters,考虑模块的情况

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

// useGetters同理

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

使用:

setup() {
const state = useState(["rootCounter"])
const rootGetters = useGetters(["doubleRootCounter"])
const getters = useGetters("home", ["doubleHomeCounter"])
}