vue3学习 --- vuex的基本使用(上)

1,928 阅读5分钟

在开发中,我们会的应用程序需要处理各种各样的数据,这些 数据需要保存在我们应用程序中的某一个位置,对于这些数据 的管理我们就称之为是 状态管理

在Vue开发中,我们使用组件化的开发方式

在组件中我们定义data或者在setup中返回使用的数据, 这些数据我们称之为state

在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View

在模块中我们会产生一些行为事件,处理这些行为事件时, 有可能会修改state,这些行为事件我们称之为actions

IHR8R4.png

JavaScript开发的应用程序,已经变得越来越复杂了,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

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

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

此时, 我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例对象的方式来管理,即将这些数据转变为全局对象来进行使用

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

IHRbA6.png

从上图可以得知在vue中,vuex有5大核心:

  1. state ---- 存储全局状态
  2. getters --- store中的计算属性
  3. mutations --- 只能是同步操作
  4. actions ---- 执行异步操作
  5. modules

mutations中只能使用同步操作,是因为vue devtool会记录mutation中的变化,形成对应的快照,以便于我们进行调试

但是如果mutation中存在异步操作数据,会导致devtool中的数据更新不一致,不利于调试

使用

# 安装 --- 如果需要使用的是vuex4.x,安装的时候需要添加 next 指定版本
npm i vuex@next

一般我们会将我们书写的操作vuex的代码存放在store文件夹下

store

创建Store

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

  • store本质上是一个全局对象,它包含着你的应用中大部分的状态(state)

Vuex和单纯的全局对象的区别:

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

main.js

import { createApp } from 'vue'
import App from './App.vue'

import router from './routes'
import store from './store'

// vuex本质上也是vue的一个插件 --- 挂载到vue上以后会在所有的实例上生产一个$store对象来帮助我们访问vuex
createApp(App).use(router).use(store).mount('#app')

v1

store.js

import { createStore } from 'vuex'

const store = createStore({
 // state是一个返回对象的函数
 // 所有的vuex数据存放在state函数返回的对象中
 state() {
   return {
     counter: 0
   }
 }
})

export default store

App.vue

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
export default {
  name: 'App',

  methods: {
    increment() {
      // 虽然这么做并不会报错,但是这样做vuex并不推荐
      this.$store.state.counter++
    },

    decrement() {
      this.$store.state.counter--
    },
  }
}
</script>

v2

store.js

import { createStore } from 'vuex'

const store = createStore({
 state() {
   return {
     counter: 0
   }
 },

 // 通过mutations函数来修改state
 mutations: {
   // mutations中的函数会被vuex在合适的时间进行回调
   // 会将当前vuex实例的state对象作为参数进行传入  
   increment(state) {
     state.counter++
   },
   decrement(state) {
    state.counter--
  }
 }
})

export default store

App.vue

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
export default {
  name: 'App',

  methods: {
    increment() {
      // 使用commit函数,触发mutations中的对应函数
      this.$store.commit('increment')
    },

    decrement() {
      this.$store.commit('decrement')
    },
  }
}
</script>

单一状态树

Vuex 使用单一状态树:

  • 用一个对象就包含了全部的应用层级别的状态
  • 采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源
  • 这也意味着,每个应用将仅仅包含一个 store 实例
  • 我们可以使用module来对store进行进一步的拆分

单一状态树的优势:

  • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难
  • 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便 的管理和维护

mapState

如果我们在模板中每次都需要通过$store.state.xxx的方式来访问store属性的话,会比较繁琐,所以vuex提供了mapState的辅助函数

<template>
  <div>
    <h2>{{ name }}</h2>
    <h2>{{ age }}</h2>
    <h2>{{ counter }}</h2>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'App',

  computed: {
    // 1. 使用方式1,使用数组作为参数进行传递
    ...mapState(['name', 'age', 'counter'])
  }
}
</script>
<template>
  <div>
    <h2>{{ sName }}</h2>
    <h2>{{ sAge }}</h2>
    <h2>{{ sCounter }}</h2>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'App',

  computed: {
    // 使用方式2: 传递对象
    // 1. 可以自定义需要使用的store成员的名称
    // 2. 可以对获取到的数据进行二次加工
    ...mapState({
      sName: state => state.name,
      sAge: state => state.age,
      sCounter: state => state.counter * 2,
    })
  }
}
</script>

mapState一般需要和computed一起结合使用

// mapState返回的是对象,key是属性名,value是一个get函数,这个get函数会通过this.$store去取对应的属性值
...mapState(['name', 'age', 'counter'])

// 实际编译后的结果是类似如下的对象(伪代码)
{
  name() {
    // mapState内部依旧是使用this.$store来取数据的
    return this,$store.name
  },
    
  age() {
    return this,$store.age
  },
    
  counter() {
    return this,$store.counter
  }
}

setup中使用mapState

hooks/useMapState.js

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

export default function(mapper) {
    // 在setup函数中可以通过useStore这个hook函数来获取store对象
    const store = useStore()

    // mapState的参数无论是对象还是数组,他们的返回值结构都是一致的
    // { key: get函数, key: get函数 }
    // 所以我们导出的方法的参数即支持对象,也支持函数
    const mapStateFns = mapState(mapper)
    const storeState = {}


    Object.keys(mapStateFns).forEach(key => {
      // 之所以需要使用computed函数进行包裹的目的是为了导出对象的value
      // 使用通过computed函数导出的ref对象,这样才可以在store中数据发生改变的时候
      // 自动进行监听,并自动更新所有的依赖 
      storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
    })

    return storeState
}

hook使用者

<template>
  <div>
    <h2>{{ name }}</h2>
    <h2>{{ age }}</h2>
  </div>
</template>

<script>
import useMapState from './hooks/useMapState'

export default {
  name: 'App',

  setup() {


    return {
      // 自定义的hook函数的参数即支持数组,也支持对象
      ...useMapState(['name']),
      ...useMapState({
        age: store => store.age
      })
    }
  }
}
</script>

getters

某些属性我们可能需要经过变化后来使用,这个时候可以使用getters

getters类似于store中的computed

基本使用

store.js

import { createStore } from 'vuex'

const store = createStore({
 state() {
   return {
     books: [
       {
         name: 'book1',
         price: 32,
         count: 3
       },

       {
        name: 'book2',
        price: 45,
        count: 5
      },

      {
        name: 'book3',
        price: 54,
        count: 2
      }
     ]
   }
 },

 getters: {
   // getters中定义的是函数
   /*
    1. 参数1: state对象
    2. 参数2: getters对象,用于在getters中进行计算的时候可以使用其它的'计算值'
   */
   totalPrice(state, getters) {
    return (state.books.reduce((total, book) => total + book.price * book.count, 0) * getters.discount).toFixed(2)
   },

   discount() {
     return 0.95
   }
 }
})

export default store

使用者

<template>
  <div>
    <!--
      和计算属性一样,虽然totalPrice是一个函数,
      但是使用的时候,像一个属性一样去使用即可
    -->
    <h2>{{ $store.getters.totalPrice }}</h2>
  </div>
</template>

很多时候我们可能需要在进行计算的时候,需要添加限制条件,

此时我们可以让getters返回一个函数,通过返回的函数来接收我们需要的参数

store.js

import { createStore } from 'vuex'

const store = createStore({
 state() {
   return {
     books: [
       {
         name: 'book1',
         price: 32,
         count: 3
       },

       {
        name: 'book2',
        price: 45,
        count: 5
      },

      {
        name: 'book3',
        price: 54,
        count: 2
      }
     ]
   }
 },

 getters: {
   totalPrice(state) {
    //  返回函数,让getter函数可以接收外界传入的参数
    return v => {
      return (state.books.reduce((total, book) => book.count < v ? total + book.price * book.count : 0, 0)).toFixed(2)
    }
   }
 }
})

export default store

使用者

<template>
  <div>
    <!-- 进行参数传递 -->
    <h2>{{ $store.getters.totalPrice(5) }}</h2>
  </div>
</template>

mapGetters

和mapStore一样,vuex提供了mapGetters函数来便于我们进行使用

数组写法

import { mapGetters } from 'vuex'

export default {
  name: 'App',

  computed: {
    ...mapGetters(['totalPrice'])
  }
}

对象语法

import { mapGetters } from 'vuex'

export default {
  name: 'App',

  computed: {
    ...mapGetters({
      // 这里的vlaue直接给key的name即可
      // 不需要传递一个函数,这和mapStore函数的对象写法是不一致的
      totalPrice: 'totalPrice'
    })
  }
}

和之前mapStore封装的hook函数一样的思路

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

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

    const mapStateFns = mapGetters(mapper)
    const storeState = {}


    Object.keys(mapStateFns).forEach(key => {
      storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
    })

    return storeState
}

将useGetters和useStore进行整合

useMapper.js

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

export default function(mapper, mapFn) {
    const store = useStore()

    const mapStateFns = mapFn === 'state' ? mapState(mapper) : mapGetters(mapper)
    const storeState = {}


    Object.keys(mapStateFns).forEach(key => {
      storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
    })

    return storeState
}

useState.js

import useMapper from './useMapper'

export default function(mapper) {
   return useMapper(mapper, 'state')
}

useGetters.js

import useMapper from './useMapper'

export default function(mapper) {
   return useMapper(mapper, 'getter')
}

index.js

import useGetters from './useMapper'
import useState from './useState'

export {
  useGetters,
  useState
}

使用者

<template>
  <div>
    <h2>{{ totalPrice(5) }}</h2>
  </div>
</template>

<script>
import { useGetters } from './hooks'

export default {
  name: 'App',

  setup() {
    return {
      ...useGetters(['totalPrice'])
    }
  }
}
</script>

mutations

参数传递

store.js

import { createStore } from 'vuex'

const store = createStore({
 state() {
   return {
     counter: 0
   }
 },
 mutations: {
   incrementN(store, payload) {
     store.counter += payload.step
   },
   decrementN(store, payload) {
    store.counter -= payload.step
   }
 }
})

export default store

使用者

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="increment">+10</button>
    <button @click="decrement">-10</button>
  </div>
</template>

<script>
import { useStore } from 'vuex'

export default {
  name: 'App',

  setup() {
    const store = useStore()

    const increment = () => store.commit('incrementN', { step: 10 })

    // 这是另一个提交方式
    const decrement = () => store.commit({
      // type属性中书写的是事件名
      type: 'decrementN',
      step: 10
    })

    return {
      increment,
      decrement
    }
  }
}
</script>

mapMutations

和mapGetters和mapStore一样,vuex为mutation提供了辅助函数mapMutations

options api中的使用

数组写法

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="incrementN({step: 10})">+10</button>
    <button @click="decrementN({step: 10})">-10</button>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  name: 'App',

  methods: {
    // 注意: mapMutations解构出来的函数是不需要交给computed的
    // 因为mapMutations返回的是类似于 {key: 事件处理函数, key: 事件处理函数} 格式的对象
    // 所以mapMutations返回的函数,可以直接合并到methods中直接使用
    ...mapMutations(['incrementN', 'decrementN'])
  }
}
</script>

对象写法

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="increment({step: 10})">+10</button>
    <button @click="decrement({step: 10})">-10</button>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  name: 'App',

  methods: {
    ...mapMutations({
      increment: 'incrementN',
      decrement: 'decrementN'
    })
  }
}
</script>

composition api中的使用

数组语法

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="incrementN({step: 10})">+10</button>
    <button @click="decrementN({step: 10})">-10</button>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  name: 'App',

  setup() {
    const mutations = mapMutations(['incrementN', 'decrementN'])

    return {
      ...mutations
    }
  }
}
</script>

对象语法

<template>
  <div>
    <h2>{{ $store.state.counter }}</h2>
    <button @click="increment({step: 10})">+10</button>
    <button @click="decrement({step: 10})">-10</button>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  name: 'App',

  setup() {
    const mutations = mapMutations({
      increment: 'incrementN',
      decrement: 'decrementN'
    })

    return {
      ...mutations
    }
  }
}
</script>