Vue状态管理--Vuex使用详解

一、状态管理概述

1.1 什么是状态管理

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

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

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

他们三者之间总是相互影响的。

因此应用程序需要处理各种各样的数据,这些数据需 要保存在我们应用程序中的某一个位置。

对于这些数据的管理就称之为状态管理。其实就是一个数据变成另一个数据的过程。

1.2 不使用状态管理所带来的的问题

  • JavaScript开发的应用程序中:

    • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等
    • 也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页
    • 上面的状态数据会随着应用程序的变大越来越难以管理和维护
  • Vue3开发的应用程序中:

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

    • 来自不同视图的行为需要变更同一状态难以完成

    • 对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态

      但是对于复杂的状态管理来说,单纯通过传递和共享的方式是不足以解决问题的

1.3 Vuex的思想和作用

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

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化

  • 当程序复杂时,state在什么时候,因为什么原因而发生变化,发生怎么样的变化,会非常难以控制和追踪

因此,Vuex将组件的内部状态抽离出来,以一个全局单例的方式来管理这些状态:

  • 在这种模式下,组件树构成了一个巨大的 视图View

  • 不管在树的哪个位置,任何组件都能获取状态或者触发行为

  • 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性

这样代码边会变得更加结构 化和易于维护、跟踪。

Vue2.x时,它是主流的状态管理工具。现在依然很多项目再使用。

Vue3.x时,Vue官方也推荐使用Pinia进行状态管理。

二、Vuex的安装

npm run vuex

三、创建Store

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

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

  • Vuex和单纯的全局对象的区别呢

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

    1)新建一个store文件夹,下面新建一个index.js

    import {
          
           createStore } from 'vuex'
    
    // createStore创建一个状态管理仓库
    const store = createStore({
          
          
      //仓库中定义着一些状态数据
      state: () => ({
          
          
        counter: 100,
      }),
        
      mutations: {
          
          
        increment(state) {
          
          
          state.counter++
      },
    })
    
    export default store
    
    

    2)main.js中注册

    import {
          
           createApp } from 'vue'
    import App from './App.vue'
    // 引入store目录下的index.js
    import store from './store'
    // use这个store,这样就可以通过$store去拿到状态管理仓库中的数据
    
    createApp(App).use(store).mount('#app')
    

    3)组件中获取状态管理仓库中数据

    <template>
      <div class="app">
        <!-- template中使用store中的counter -->
        <h2>App当前计数: {
          
          {
          
           $store.state.counter }}</h2>
      </div>
    </template>
    
    <script setup>
    
      import {
          
          useStore} from 'vuex'
      // js代码中获取store中的counter
      const store = useStore();
      const conut = store.state.counter
      //调用mutations中的increment函数去修改counter
      function increment() {
          
          
        store.commit("increment")
      }
    </script>
    
  • 在组件中使用store,一般有如下几种情况

    • 在模板中使用

      <template>
        <div class="app">
          <!-- template中使用store中的counter -->
          <h2>App当前计数: {
              
              {
              
               $store.state.counter }}</h2>
        </div>
      </template>
      
    • setup中使用

      <script setup>
        import {
              
              toRefs} from 'vue'
        import {
              
              useStore} from 'vuex'
        // js代码中获取store中的counter
        const store = useStore();
        // 这么赋值后数据conut不是响应式的,只是个普通js变量
        const conut = store.state.counter
        // 使用toRefs去解构,解构出来的数据还是响应式的
        const countRef = toRefs(store.state)
        
        //调用mutations中的increment函数去修改counter
        function increment() {
              
              
          store.commit("increment")
        }
      </script>
      
    • options api中使用,比如computed

      <script>
          export default {
              
              
              computed: {
              
              
                  storeCounter() {
              
              
                      return this.$store.state.counter
                  }
              }
          }
      </script>
      

四、单一状态树

Vuex使用单一状态树: 即用一个对象就包含了全部的应用层级的状态。

这也意味着,每个应用将仅仅包含一个store实例。

单一状态树的优势:

  • 如果状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难;

    所以Vuex也使用了单一状态树来管理应用层级的全部状态;

  • 单一状态树能够让我们最直接的方式找到某个状态的片段;

  • 而且在之后的维护和调试过程中,也可以非常方便的管理和维护;

五、获取多个状态数据–mapState的使用

如果有很多个状态都需要获取话,可以使用mapState的辅助函数:

  • mapState的方式一:数组类型;

    • 旧写法
    <script>
      import {
          
           mapState } from 'vuex'
    
      export default {
          
          
        computed: {
          
          
          ...mapState(["name", "level", "avatarURL"]),
        }
      }
    </script>
    
  • mapState的方式二:对象类型;

    • 旧写法
    <script>
      import {
          
           mapState } from 'vuex'
    
      export default {
          
          
        computed: {
          
          
          ...mapState({
          
          
            sName: state => state.name,
            sLevel: state => state.level
          })
        }
      }
    </script>
    
    • 主流写法

      mapStatesetup中并不好用,如果使用组合式API一般都按下面的方式写

    <script setup>
      import {
          
           toRefs } from 'vue'
      import {
          
           useStore } from 'vuex'
        
      // 直接对store.state进行解构(推荐)
      const store = useStore()
      const {
          
           name, level } = toRefs(store.state)
    
      function incrementLevel() {
          
          
        store.state.level++
      }
    
    </script>
    

从mapState的使用也可以看出来,Vuex适用的是选项式API,也就是适用于Vue2.x

而组合式API中用起来会很别扭,Vue3.x更加契合pinia

六、getters的使用

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

6.1 基本使用

getters可以传入两个参数:

  • state:可以用来获取到state中定义的数据
  • getters:可以直接获取其它的getters
import {
    
     createStore } from 'vuex'

// createStore创建一个状态管理仓库
const store = createStore({
    
    
  //仓库中定义着一些状态数据
  state: () => ({
    
    
    counter: 100,
    name: "张三",
    level: 99,
    avatarURL: "http://xxxxxx",
    friends: [
      {
    
     id: 111, name: "why", age: 20 },
      {
    
     id: 112, name: "kobe", age: 30 },
      {
    
     id: 113, name: "james", age: 25 }
    ]
  }),
  getters: {
    
    
    // getters基本使用
    doubleCounter(state) {
    
    
      return state.counter * 2
    },
    totalAge(state) {
    
    
      return state.friends.reduce((preValue, item) => {
    
    
        return preValue + item.age
      }, 0)
    },
    // 2.在该getters属性中, 获取其他的getters
    message(state, getters) {
    
    
      return `name:${
      
      state.name} level:${
      
      state.level} friendTotalAge:${
      
      getters.totalAge}`
    },
    // 3.getters是可以返回一个函数的, 调用这个函数可以传入参数(了解)
    getFriendById(state) {
    
    
      return function(id) {
    
    
        const friend = state.friends.find(item => item.id === id)
        return friend
      }
    }
  }
})

export default store
<template>
  <div class="app">
    <h2>doubleCounter: {
    
    {
    
     $store.getters.doubleCounter }}</h2>
    <h2>friendsTotalAge: {
    
    {
    
     $store.getters.totalAge }}</h2>
    <h2>message: {
    
    {
    
     $store.getters.message }}</h2>

    <!-- 根据id获取某一个朋友的信息 -->
    <h2>id-111的朋友信息: {
    
    {
    
     $store.getters.getFriendById(111) }}</h2>
    <h2>id-112的朋友信息: {
    
    {
    
     $store.getters.getFriendById(112) }}</h2>
  </div>
</template>

其实这里也能看出来,这个getters和计算属性是非常相似的

6.2 Getters映射–mapGetters的使用

选项式API:

<script>
  import {
    
     mapGetters } from 'vuex'

  export default {
    
    
    computed: {
    
    
      ...mapGetters(["doubleCounter", "totalAge"]),
      ...mapGetters(["getFriendById"])
    }
  }
</script>

组合式API:

<script setup>

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

  const store = useStore()

  // 1.使用mapGetters(较麻烦,不推荐)
  // const { message: messageFn } = mapGetters(["message"])
  // const message = computed(messageFn.bind({ $store: store }))

  // 2.直接解构, 并且包裹成ref
  // const { message } = toRefs(store.getters)

  // 3.针对某一个getters属性使用computed(推荐写法)
  const message = computed(() => store.getters.message)

  function changeAge() {
    
                                                                                                                                                                          
    store.state.name = "kobe"
  }

</script>

七、Mutation的使用

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

7.1 Mutation的基本使用

import {
    
     createStore } from 'vuex'

const store = createStore({
    
    
  state: () => ({
    
    
    counter: 100,
    name: "张三",
    level: 100
  }),
  // 定义可以被调用的mutations
  mutations: {
    
    
    increment(state) {
    
    
      state.counter++
    },
    changeName(state, payload) {
    
    
      state.name = payload
    },
    incrementLevel(state) {
    
    
      state.level++
    },
    // newInfo是传递过来的参数
    changeInfo(state, newInfo) {
    
    
      state.level = newInfo.level
      state.name = newInfo.name
    }
  }
})

export default store
<template>
  <div class="app">
    <button @click="changeName">修改name</button>
    <button @click="incrementLevel">递增level</button>
    <button @click="changeInfo">修改info</button>
    <h2>Store Name: {
    
    {
    
     $store.state.name }}</h2>
    <h2>Store Level: {
    
    {
    
     $store.state.level }}</h2>
  </div>
</template>

<script>

  import {
    
     CHANGE_INFO } from "@/store/mutation_types"

  export default {
    
    
    methods: {
    
    
      changeName() {
    
    
        this.$store.commit("changeName", "王小波")
      },
      incrementLevel() {
    
    
        this.$store.commit("incrementLevel")
      },
      changeInfo() {
    
    
        this.$store.commit(changeInfo, {
    
    
          name: "王二",
          level: 200
        })
      }
    }
  }
</script>

7.2 Mutation定义常量类型

1)抽取常量到一个js文件中

export const CHANGE_INFO = "changeInfo"

2)使用常量

import {
    
     createStore } from 'vuex'
import {
    
     CHANGE_INFO } from './mutation_types'	//导入定义常量的js

const store = createStore({
    
    
  state: () => ({
    
    
    name: "张三",
    level: 100
  }),
  // 定义可以被调用的mutations
  mutations: {
    
    
    // newInfo是传递过来的参数
    changeInfo(state, newInfo) {
    
    
      state.level = newInfo.level
      state.name = newInfo.name
    }
  }
})

export default store
<script>
  // 导入常量
  import {
    
     CHANGE_INFO } from "@/store/mutation_types"

  export default {
    
    
    methods: {
    
    
      changeInfo() {
    
    
        // 使用常量
        this.$store.commit(CHANGE_INFO, {
    
    
          name: "王二",
          level: 200
        })
      }
    }
  }
</script>

这也是Vue推荐的做法

7.3 mapMutations辅助函数

选项式API中:

<script>
  import {
    
     mapMutations } from 'vuex'
  import {
    
     CHANGE_INFO } from "@/store/mutation_types"

  export default {
    
    
    computed: {
    
    
    },
    methods: {
    
    
      ...mapMutations(["changeName", "incrementLevel", CHANGE_INFO])
    }
  }
</script>

组合式API中:

<script setup>

  import {
    
     mapMutations, useStore } from 'vuex'
  import {
    
     CHANGE_INFO } from "@/store/mutation_types"

  const store = useStore()

  // 手动的映射和绑定
  const mutations = mapMutations(["changeName", "incrementLevel", CHANGE_INFO])
  const newMutations = {
    
    }
  Object.keys(mutations).forEach(key => {
    
    
    newMutations[key] = mutations[key].bind({
    
     $store: store })
  })
  const {
    
     changeName, incrementLevel, changeInfo } = newMutations

</script>

Vuex确实不太适合在Vue3的组合式API中使用

7.4 mutation重要原则

mutation必须是同步函数 ,这意味着不可以在mutation方法中进行异步操作。

  • 因为devtool工具会记录mutation的日记;

  • 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;

  • 但是在mutation中执行异步操作,就无法追踪到数据的变化;

是如果我们希望在Vuex中发送异步·网络请求的话,可以使用actions

八、actions的使用

Action类似于mutation,不同在于:

  • Action提交的是mutation,而不是直接变更状态,也就是说,想要修改状态数据,必须经过mutation
  • Action可以包含任意异步操作;

8.1 基本使用

import {
    
     createStore } from 'vuex'

const store = createStore({
    
    
  state: () => ({
    
    
    // 模拟数据
    counter: 100,
    name: "张三",
    level: 100,
  }),

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

  },
  actions: {
    
    
    incrementAction(context) {
    
    
      // console.log(context.commit) 	// 用于提交mutation
      // console.log(context.getters) 	// 用于使用getters
      // console.log(context.state) 	// 用于使用state
      context.commit("increment")
    },
    changeNameAction(context, payload) {
    
    
      context.commit("changeName", payload)
    },
  }
})

export default store

<template>
  <div class="home">
    <h2>当前计数: {
    
    {
    
     $store.state.counter }}</h2>
    <button @click="counterBtnClick">发起action修改counter</button>
    <h2>name: {
    
    {
    
     $store.state.name }}</h2>
    <button @click="nameBtnClick">发起action修改name</button>
  </div>
</template>

<script>

  export default {
    
    
    methods: {
    
    
      counterBtnClick() {
    
    
        // 调用一个action的话 需要使用dispatch方法
        this.$store.dispatch("incrementAction")
      },
      nameBtnClick() {
    
    
        // 向action中闯入参数的写法
        this.$store.dispatch("changeNameAction", "aaa")
      }
    }
  }
</script>

actions中的函数有一个非常重要的参数context

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

  • context可以获取到commit方法来提交一个mutation

    也可以通context.statecontext.getters来获取stategetters

使用action时,进行action分发的方法:

  • 不带参: this.$store.dispatch("incrementAction")

  • 带参: this.$store.dispatch("changeNameAction", {count:100})

  • 直接传递对象:

    this.$store.dispatch({
    	type: "increment",
    	count: 100
    })
    

8.2 actions辅助函数–mapActions

  • 对象类型

    methods: {
          
          
      ...mapActions( ["increment" , "decrement"]),
      .. .mapActions({
          
          
        add : "increment" ,
        sub: "decrement"
      })
    }
    
  • 数组类型

    methods: {
          
          
      ...mapActions( ["increment" , "decrement"])
    }
    
  • 组合式API中的写法

    <script setup>
    
      import {
          
           useStore, mapActions } from 'vuex'
    
      const store = useStore()
    
      // 1.在setup中使用mapActions辅助函数
      // const actions = mapActions(["incrementAction", "changeNameAction"])
      // const newActions = {}
      // Object.keys(actions).forEach(key => {
          
          
      //   newActions[key] = actions[key].bind({ $store: store })
      // })
      // const { incrementAction, changeNameAction } = newActions
    
      // 2.使用默认的做法
      function increment() {
          
          
        store.dispatch("incrementAction")
      }
    
    </script>
    

    组合式API中使用这个是比较复杂的

8.3 actions中的异步操作

import {
    
     createStore } from 'vuex'

const store = createStore({
    
    
  state: () => ({
    
    
    // 服务器数据
    banners: [],
  }),

  mutations: {
    
    
    changeBanners(state, banners) {
    
    
       state.banners = banners
    },
  },
  actions: {
    
    
   
    
    fetchHomeMultidataAction(context) {
    
    
        //方式一: Promise链式调用
        /*
        fetch("http://123.207.32.32:8000/home/multidata").then(res => {
           return res.json()
        }).then(data => {
           console.log(data)
        })
        */
        
        // 方式二:
         return new Promise(async (resolve, reject) => {
    
    
            // 3.await/async
            const res = await fetch("http://123.207.32.32:8000/home/multidata")
            const data = await res.json()

            // 修改state数据
            context.commit("changeBanners", data.data.banner.list)
            resolve("test")
        }
    }

      

}
  },
  modules: {
    
    
    home: homeModule,
    counter: counterModule
  }
})

export default store

<template>
  <div class="home">
    <h2>Home Page</h2>
    <ul>
      <!-- 使用state中查询出来的数据 -->
      <template v-for="item in $store.state.banners" :key="item.acm">
        <li>{
    
    {
    
     item.title }}</li>
      </template>
    </ul>
  </div>
</template>
<script setup>

  import {
    
     useStore } from 'vuex'

  // 告诉Vuex发起网络请求
  const store = useStore()
  store.dispatch("fetchHomeMultidataAction").then(res => {
    
    
    console.log("home中的then被回调:", res)
  })

</script>

九、module的使用

由于使用单一状态树,应用的所有状态会集中成一个比较大的对象。

当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。

每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块.

9.1 基本使用

1)新建一个module目录,下面新建一个home.js

home.vue组件中用到的状态数据写到这个js中去

export default {
    
    
  state: () => ({
    
    
    count: 0,
    // 服务器数据
    banners: []
  }),
  getters: {
    
    
    doubleCount(state, getters, rootState) {
    
    
      return state.count + rootState.rootCounter
    }
  },
    
  mutations: {
    
    
    incrementCount(state) {
    
    
      console.log(state)
      state.count++
    }
    changeBanners(state, banners) {
    
    
      state.banners = banners
    }
  },
  actions: 
    
    incrementCountAction(context) {
    
    
      context.commit("incrementCount")
    }
    fetchHomeMultidataAction(context) {
    
    
      return new Promise(async (resolve, reject) => {
    
    
        // await/async
        const res = await fetch("http://123.207.32.32:8000/home/multidata")
        const data = await res.json()
        
        // 修改state数据
        context.commit("changeBanners", data.data.banner.list)

        resolve("aaaaa")
      })
    }
  }
}

2)store主文件中使用定义好的module

import {
    
     createStore } from 'vuex'
// 导入上面的js文件
import homeModule from './modules/home'

const store = createStore({
    
    
  state: () => ({
    
    
    rootCounter: 100,
  }),
  modules: {
    
    
    home: homeModule,	//使用定义好的module
  }
})

export default store

3)模板中使用

<template>
  <div class="home">
    <h2>Home Page</h2>
    <!-- 1.使用state时, 是需要state.moduleName.xxx -->
    <h2>Counter模块的counter: {
    
    {
    
     $store.state.counter.count }}</h2>
    <!-- 2.使用getters时, 是直接getters.xxx -->
    <h2>Counter模块的doubleCounter: {
    
    {
    
     $store.getters.doubleCount }}</h2>

    <button @click="incrementCount">count模块+1</button>
  </div>
</template>

<script setup>

  import {
    
     useStore } from 'vuex'

  // 告诉Vuex发起网络请求
  const store = useStore()
  // 派发事件时, 默认也是不需要跟模块名称
  // 提交mutation时, 默认也是不需要跟模块名称
  function incrementCount() {
    
    
    store.dispatch("incrementCountAction")
  }

</script>

使用模块中的状态数据,都不需要写模块名称。默认这些模块都会合并到主文件当中的。

9.2 module的命名空间

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

这样使得多个模块能够对同一个 action 或 mutation 作出响应; Getter 同样也默认注册在全局命名空间。

但是这样可能会出现各种命名重复的问题。

如果希望模块具有更高的封装度和复用性,可以添加namespaced: true 的方式使其成为带命名空间的模块。

当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。

export default {
    
    
  namespaced: true,		//加上这句话它就有了自己的命名空间
  state: () => ({
    
    
    count: 0,
    // 服务器数据
    banners: []
  }),

}

使用时就需要加上模块名,也就是js文件的文件名:

<template>
  <div class="home">
	<h2>Counter模块的doubleCounter: {
    
    {
    
     $store.getters["counter/doubleCount"] }}</h2>
  <div>
</emplate>

9.3 module修改或派发根组件

就是模块中调用和修改根组件当中的东西。

//这里一共有六个参数
changeNameAction({
     
     commit,dispatch, state, rootState, getters, rootGetters}) {
    
    
    commit( "changeName", "kobe");
    // 重要的就是加上root: true即可
	commit( "changeRootName", null, {
    
    root: true});
	dispatch("changeRootNameAction", null, {
    
    root: true})
}

猜你喜欢

转载自blog.csdn.net/qq_44749491/article/details/127216717