【校招VIP】Vue全家桶 - Vuex的理解和学习

02月07日 收藏 0 评论 0 前端开发

【校招VIP】Vue全家桶 - Vuex的理解和学习

转载声明:文章来源https://blog.csdn.net/weixin_54092687/article/details/140280584

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

因为mapActions和mapMutations原因一致,所以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。

命名空间
默认情况下,模块内部的 action 和 mutation 和仍然是注册在全局命名空间的。这样使得多个模块能够对同一个 action 或 mutation 作出响应。
getter 同样也默认注册在全局命名空间。
注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望模块具有更高的封装度和复用性,通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

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。

在带命名空间的模块内访问全局内容
如果希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,需将 { root: true } 作为第三参数传给 dispatch 或 commit 。

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 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。所以应该根据应用开发需要进行权衡和确定。

C 0条回复 评论

帖子还没人回复快来抢沙发