Vue全家桶 - Vuex的理解和学习

Vuex

什么是Vuex?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
什么情况下使用Vuex?
Vuex 适合于那些需要集中管理复杂状态、‌跨组件或跨页面共享数据的中大型 Vue.js 应用。‌所以当需要构建一个中大型单页应用,‌并且需要在组件外部更好地管理状态时使用它是个很好的选择。‌
Vuex 和单纯的全局对象的不同?
Vuex 的状态存储是响应式的。
不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。

Vuex官方文档https://vuex.vuejs.org/zh/

state

// 在store/index.js 创建数据
import { createStore } from 'vuex'
export default createStore({
  state: {
    count: 0,
    message: '数据'
  }
})
// vue2
<template>
  <p>count:{{ $store.state.count }}</p>
</template>
<script>
computed: {
    count () {
      return this.$store.state.count
    }
  }
</script>
// vue3
<template>
  <p>count:{{ store.state.count }}</p>
</template>
<script setup>
  import { computed } from 'vue'
  import { useStore } from 'vuex'
  const store = useStore();
  const count = computed(() => store.state.count)
</script>

mapState

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

// vue2 -- 写法1
import { mapState } from 'vuex'
export default {
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,
    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',
    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}
// vue2 -- 写法1
// 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: mapState(['count']) // 映射 this.count 为 store.state.count
// vue3中按照vue2的形式
<script setup>
  import {  mapState } from 'vuex'
  const storeStateFns = mapState(['count', 'message'])
  console.log(storeStateFns)
</script>

在这里插入图片描述

vue3使用辅助函数后返回为什么是函数?
因为mapState辅助函数是用this.$store 来拿到 store 的值的,但在setup中是娶不到this的,因此辅助函数就是会返回一个对象,而 key 是字符串, val就是函数。

使用 computedmapState 封装一个 hooks

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

// mapper类型Array | Object
const useState = function(mapper) {
    const store = useStore()
    
    //使用辅助函数解析成一个对象
    const storeStateFns = mapState(mapper)
    
    const storeState = {}
    //通过Object.keys拿到对象的所有key值,遍历,取出对应的value值,也就是函数
    Object.keys(storeStateFns).forEach(item => {
        // 这我们知道辅助函数的内部是通过this.$store来实现的
        // setup中没有this, 所以通过bind来改变this的指向
        const fn = storeStateFns[item].bind({ $store: store })
        //拿到函数,作为计算属性的参数,最后在留在一个对象中
        storeState[item] = computed(fn)
    })
    
    // storeState是一个对象, key是字符串, value值是ref对象
    return storeState;
}

export default useState;
// 页面使用
<script setup>
  import useState from './store/useState'
  const stateStore = useState(['count', 'message']) // 使用对象的形式:useState({ count: 'count' })
  console.log(stateStore.count.value)
</script>

Getter

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。

// 定义getters
import { createStore } from 'vuex'
export default createStore({
  state: {
    todos: [
      { id: 1, count: 1, text: 'todo1', done: true },
      { id: 2, count: 2, text: 'todo2', done: false }
    ]
  },
  getters: {
    arrLen: (state, getters) => {
      return state.todos.length + getters.getTodoById.length
    },
    getTodoById: (state) => (id: any) => {
      return state.todos.find(todo => todo.id === id)
    }
  }
})
// vue2 
<script>
  export default {
    mounted() {
      console.log(this.$store.getters.arrLen)
      console.log(this.$store.getters.getTodoById(2))
    }
  }
</script>
// vue3
<script setup>
  import { useStore } from 'vuex'
  const store = useStore();
  console.log(store.getters.arrLen)
  console.log(store.getters.getTodoById(2))
</script>

mapGetters

// vue2 -- 方式1
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['arrLen', 'getTodoById'])
  }
}
// vue2 -- 方式2
// 把 `this.doneCount` 映射为 `this.$store.getters.arrLen`
...mapGetters({doneCount: 'doneTodosCount'})

在vue3中使用mapGetters和使用getState方式一致,需要使用 computedmapGetters 封装一个 hooks

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

const useGetter = (mapper: any, mapFn: any) => {
  const store = useStore()

  const storeStateFns = mapFn(mapper)
  const storeState: any = {}
  Object.keys(storeStateFns).forEach((keyFn) => {
    const fn = storeStateFns[keyFn].bind({ $store: store })
    storeState[keyFn] = computed(fn)
  })

  return storeState
}

export const useState = (mapper: any) => {
  return useGetter(mapper, mapState)
}

export const useGetters = (mapper: any) => {
  return useGetter(mapper, mapGetters)
}
// 使用useGetters
<script setup>
  import { useStore } from 'vuex'
  import { useGetters } from '@/store/useGetter';
  const store = useStore();
  const storeGetters = useGetters(['arrLen', 'getTodoById'])
  console.log(storeGetters.arrLen.value) // -- 3
  console.log(storeGetters.getTodoById.value(1).text) // -- todo1
</script>

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。

// 定义mutation
import { createStore } from 'vuex'
export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment: (state) => {
      state.count++
    }increment2: (state, n) => {
    state.count += n
  }
})
// vue2
<template>
  <button @click="$store.commit('increment')">increment</button>
  <button @click="$store.commit('increment2', 10)">increment2</button> // 提交载荷
  <p>count:{{ $store.state.count }}</p>
</template>
// vue3
<template>
  <button @click="store.commit('increment');">increment</button>
  <button @click="store.commit('increment2', 1100);">increment2</button>
  <p>count:{{ store.state.count }}</p>
</template>
<script setup>
  import { useStore } from 'vuex'
  const store = useStore();
</script>

注意: mutation 必须是同步函数。
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读。
increment2 (state, payload) { state.count += payload.amount }
$store.commit('increment2', {amount: 10})

mapMutations

// vue2
<template>
  <button @click="increment">increment</button>
  <button @click="increment2(100)">increment2</button>
  <button @click="add(200)">add</button>
  <p>count:{{ $store.state.count }}</p>
</template>

<script>
  import { mapMutations } from 'vuex';
  export default {
    methods: {
      ...mapMutations([
        'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
        'increment2' // 将 `this.increment2(amount)` 映射为 `this.$store.commit('increment2', amount)`
      ]),
      ...mapMutations({
        add: 'increment2' // 将 `this.add(amount)` 映射为 `this.$store.commit('increment2', amount)`
      })
    }
  }
</script>

为什么在vue3中不需要封装mapMutations?
在Vuex4中,mapMutations不再是必需的,因为我们可以直接在模板中使用store.commit('mutationName'),或者在组件方法中调用store.commit。封装mapMutations的原因是为了简化提交mutations的过程,但在Vuex 4中,这个过程已经足够简洁,因此不需要额外的封装。

Action

Action 类似于 mutation,但不同的是:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。

// 定义Action
import { createStore } from 'vuex'
export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment: (state) => {
      state.count++
    },
    increment2: (state, n) => {
      state.count += n
    }
  },
  actions: {
  /** 
  * Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象
  * 调用 context.commit 提交一个 mutation
  * context.state获取 state
  * context.getters获取 getters
  */
    increment: (context) => {
      context.commit('increment')
    },
    increment2: (context, payload) => {
      context.commit('increment2', payload.amount)
    }
  }
})
// vue2
<template>
  <button @click="$store.dispatch('increment')">increment</button>
  <button @click="$store.dispatch('increment2', { amount: 666 })">increment</button>
  <p>count:{{ $store.state.count }}</p>
</template>
// vue3
<template>
  <button @click="store.dispatch('increment');">increment</button>
  <button @click="store.dispatch('increment2', { amount: 888 });">increment2</button>
  <p>count:{{ store.state.count }}</p>
</template>
<script setup>
  import { useStore } from 'vuex'
  const store = useStore();
</script>

使用action实现异步操作

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

mapActions

// vue2
<template>
  <button @click="increment">increment</button>
  <button @click="increment2({ amount: 111 })">increment2</button>
  <button @click="Async">Async</button>
  <p>count:{{ $store.state.count }}</p>
</template>
<script>
  import { mapActions } from 'vuex';
  export default {
    methods: {
      ...mapActions([
        'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
        'increment2' // 将 `this.increment2(payload)` 映射为 `this.$store.dispatch('increment2', payload)`
      ]),
      ...mapActions({
        Async: 'incrementAsync' // 将 `this.Async()` 映射为 `this.$store.dispatch('incrementAsync')`
      })
    }
  }
</script>

因为mapActionsmapMutations原因一致,所以mapActions也不需要封装。

Module

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

// 定义Module
const moduleA = {
  state: {
    count: 1
  },
  getters: {},
  mutations: {},
  actions: {}
}
const moduleB = {
  state: {
    count: 2
  },
  getters: {},
  mutations: {},
  actions: {}
}
export default createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
// vue2 
<template>
  <div>
    <p>MoudleA:{{ $store.state.a.count }}</p>
    <p>MoudleB:{{ $store.state.b.count }}</p>
  </div>
</template>
// vue3
<template>
  <p>MoudleA:{{ store.state.a.count }}</p>
  <p>MoudleB:{{ store.state.b.count }}</p>
</template>
<script setup>
  import { useStore } from 'vuex'
  const store = useStore();
</script>

模块的局部变量和参数

const moduleA = {
  getters: {
    doubleCount: (state, getters, rootState) => {}
  },
  mutations: {
    increment: (state) => {}
  },
  actions: {
    logContext: ({ state, commit, rootState }) => {}
  }
}
const moduleB = {
  getters: {
    moduleBGetter: (state) => { }
  }
}

模块内部的 getter,接收的第一个参数是模块的局部状态对象,根节点状态会作为第三个参数暴露出来。
模块内部的 mutation,接收的第一个参数是模块的局部状态对象。
模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态为 context.rootState

命名空间

默认情况下,模块内部的 actionmutation 和仍然是注册在全局命名空间的。这样使得多个模块能够对同一个 actionmutation 作出响应。
getter 同样也默认注册在全局命名空间。
注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。

如果希望模块具有更高的封装度和复用性,通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。

import { createStore } from 'vuex'

export default 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。

在带命名空间的模块内访问全局内容

如果希望使用全局 stategetter,rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action
若需要在全局命名空间内分发 action 或提交 mutation,需将 { root: true } 作为第三参数传给 dispatchcommit

import { createStore } from 'vuex'
export default createStore({
  modules: {
    foo: {
      namespaced: true,
      getters: {
        // 在这个模块的 getter 中,`getters` 被局部化了
        // 你可以使用 getter 的第四个参数来调用 `rootGetters`
        someGetter(state, getters, rootState, rootGetters) {
          getters.someOtherGetter // -> 'foo/someOtherGetter'
          rootGetters.someOtherGetter // -> 'someOtherGetter'
          rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
        },
        someOtherGetter: state => { }
      },
      actions: {
        // 在这个模块中, dispatch 和 commit 也被局部化了
        // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
        someAction({ dispatch, commit, getters, rootGetters }) {
          getters.someGetter // -> 'foo/someGetter'
          rootGetters.someGetter // -> 'someGetter'
          rootGetters['bar/someGetter'] // -> 'bar/someGetter'

          dispatch('someOtherAction') // -> 'foo/someOtherAction'
          dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

          commit('someMutation') // -> 'foo/someMutation'
          commit('someMutation', null, { root: true }) // -> 'someMutation'
        },
        someOtherAction(ctx, payload) { }
      }
    }
  }
})

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。

import { createStore } from 'vuex'
export default createStore({
  actions: {
    someOtherAction({ dispatch }) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,
      actions: {
        someAction: {
          root: true,
          handler(namespacedContext, payload) { } // -> 'someAction'
        }
      }
    }
  }
})

带命名空间的绑定函数

import { createStore } from 'vuex'
export default createStore({
  modules: {
    country: {
      namespaced: true,
      modules: {
        province: {
          namespaced: true,
          modules: {
            city: {
              namespaced: true,
              state: {
                textA: '中国-山东-青岛',
                textB: '中国-山东-临沂'
              },
              getters: {
                getA: (state) => { console.log(state.textA) },
                getB: (state) => { console.log(state.textB) }
              },
              mutations: {
                fooA: (state) => { console.log(state.textA) },
                fooB: (state) => { console.log(state.textB) }
              },
              actions: {
                barA: ({ commit }) => { commit('fooA') },
                barB: ({ commit }) => { commit('fooB') }
              }
            }
          }
        }
      }
    }
  }
})
// 使用
<script>
  import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
  export default {
    computed: {
      ...mapState({
        a: state => state.country.province.city.textA,
        b: state => state.country.province.city.textB
      }),
      ...mapGetters({
        getA: 'country/province/city/getA',
        getB: 'country/province/city/getB',
      })
    },
    methods: {
      ...mapMutations({
        fooA: 'country/province/city/fooA',
        fooB: 'country/province/city/fooB'
      }),
      ...mapActions({
        barA: 'country/province/city/barA',
        barB: 'country/province/city/barB'
      })
    }
  }
</script>

如果按照之前的写法会非常繁琐,对于这种情况,可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。

<script>
  import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
  export default {
    computed: {
      ...mapState('country/province/city', {
        a: state => state.textA,
        b: state => state.textB
      }),
      ...mapGetters('country/province/city', ['getA', 'getB'])
    },
    methods: {
      ...mapMutations('country/province/city', ['fooA', 'fooB']),
      ...mapActions('country/province/city', ['barA', 'barB'])
    }
  }
</script>

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

<script>
  import { createNamespacedHelpers } from 'vuex'
  const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers('country/province/city')
  export default {
    computed: {
      ...mapState({
        a: state => state.textA,
        b: state => state.textB
      }),
      ...mapGetters(['getA', 'getB'])
    },
    methods: {
      ...mapMutations(['fooA', 'fooB']),
      ...mapActions(['barA', 'barB'])
    }
  }
</script>

#总结

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

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
尚硅谷是一家技术培训机构,他们提供了一套完整的Vue全家桶资料。所谓Vue全家桶,指的是Vue.js及其相关的生态环境,包括Vue.jsVue Router、Vuex和Axios。 首先,尚硅谷提供了Vue.js学习资料。这些资料详细介绍了Vue.js的基本知识,包括Vue.js的核心概念、指令、组件、事件处理等。学习者可以通过这些资料快速入门Vue.js,并能够了解其基本使用方式。 其次,尚硅谷还提供了Vue Router的学习资料。Vue Router是Vue.js官方推荐的前端路由库,用于实现单页应用的路由管理。尚硅谷的资料详细介绍了Vue Router的使用方法,包括路由配置、路由跳转、路由传参等,帮助学习者掌握Vue Router的基本用法。 此外,尚硅谷也提供了Vuex学习资料。VuexVue.js的状态管理库,用于集中管理应用的状态。尚硅谷的资料介绍了Vuex的核心概念、状态管理模式、数据读写等内容,帮助学习者了解Vuex的基本原理和使用方式。 最后,尚硅谷还提供了Axios的学习资料。Axios是一个基于Promise的HTTP客户端,用于发送Ajax请求并处理响应。尚硅谷的资料介绍了Axios的基本用法,包括发送GET请求、发送POST请求、处理响应等内容,帮助学习者使用Axios进行数据交互。 总之,尚硅谷提供的Vue全家桶资料非常丰富,内容详细、易于理解,对于想要学习Vue.js及其相关技术的人来说,是一份很好的学习资料。这些资料的学习将帮助学习者掌握Vue全家桶的基本概念和使用方法,从而能够快速开发出高质量的前端应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

baosy_web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值