为什么需要状态管理
状态自管理应用包含以下几个部分:
- 状态(Action),驱动应用的数据源;
- 视图(View),以声明方式将状态映射到视图;
- 操作(State),响应在视图上的用户输入导致的状态变化。
“单向数据流”理念的简单示意:
Vue 最重要就是 数据驱动 和 组件化,每个组件都有自己 data ,template 和 methods, data是数据,我们也叫做状态,通过methods中方法改变状态来更新视图,在单个组件中修改状态更新视图是很方便的,但是实际开发中是多个组件(还有多层组件嵌套)共享同一个状态
兄弟组件需要通信,这个时候传参就会很繁琐,就需要进行状态管理,负责组件中的通信,方便维护代码。
需要注意的点:
- 改变状态的唯一途径就是提交mutations
- 状态集中到 store 对象中
- 如果是异步的,就派发(dispatch)actions,其本质还是提交mutations
- 怎样去触发actions呢?可以用组件
Vue Components使用dispatch或者后端接口去触发 - 提交mutations后,可以动态的渲染组件
Vue Components
Vuex、Pinia
Pinia 和 Vuex 都是 vue 的全局状态管理器。其实Pinia就是Vuex5,只不过为了尊重原作者的贡献就沿用了这个看起来很甜的名字Pinia。
Vuex、Pinia 主要解决的问题
- 多个视图依赖同一个状态
- 来自不同视图的行为需要变更同一个状态
使用 Vuex、Pinia 的好处
- 能够在 vuex 中集中管理共享的数据,易于开发和后期维护
- 能够高效地实现组件之间的数据共享,提高开发效率
- 在 vuex 中的数据都是响应式的
Vuex使用
安装
通过yarn create vite创建vue工程,安装vuex
yarn add vuex
引入一个vuex只需要在src下创建一个store文件夹,就相当于我们当前应用的所有状态都放在整个store里面。
// store/index.js
import { createStore } from 'vuex';
const defaultState = {
count: 0,
};
export default createStore({
state() { //存放数据
return defaultState;
},
mutations: {},
actions: {},
getters:{},
});
// main.js 以插件的形式引入(通过use的方式),将其交给我们的vue
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
createApp(App).use(store).mount('#app');//这样我们就可以在vue工程里面使用vuex
最简单的Store
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。 当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
// store/index.js
import { createStore } from 'vuex';
const defaultState = {
count: 0,
};
export default createStore({
state() { //存放数据
return defaultState;
},
mutations: { //更改我们存放的数据的唯一方式就算提交mutations
increment(state) {
return state.count++;
},
},
actions: {},
getters:{
double:state => state.count * 2,
},
});
//去组件中引入一下
//components/HelloWorld.vue
<script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
const store=useStore()
const {state}=store
const countDouble=computed(() => store.getters.dounle)
console.log(state)
</script>
<template>
<div class="card">
<p>{{countDouble}}</p>
<button type="button" @click="count++">count is {{state.count}}</button>
</div>
</template>
单一状态树
store就相当于我们的一个单一状态树
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
在 Vue 组件中获得 Vuex 状态
如何在 Vue 组件中展示状态呢?
由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在 计算属性 中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。
//更新 Counter 的实现
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
mapState 辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,可以使用 mapState 辅助函数帮助我们生成计算属性:
// 在单独构建的版本中辅助函数为 Vuex.mapState
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
}
})
}
对象展开运算符
我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符,我们可以极大地简化写法:
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
组件仍然保有局部状态
使用 Vuex 并不意味着需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。具体根据应用开发需要进行权衡和确定。
Getter
有时候我们需要从 store 中的 state 中派生出一些状态,比如对列表进行过滤并计数:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,但无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。 Getter 接受 state 作为其第一个参数:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})
通过属性访问
Getter 会暴露为 store.getters 对象,可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
通过方法访问
const defaultState={
todos:[{
id:1,
text:'...',
done:false,
}]
}
export default createStore({
state(){
return defaultState
},
actions:{},
getters: {
getTodoById: (state) => (id) => {
return state.todos.find((todo) => todo.id === id)
}
}
})
//HelloWorld.vue
<script setup>
...
console.log(store.getters.getTodoById(1)) // -> { id: 1, text: '...', done: false }
</script>
getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
mapGetters 辅助函数
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
Vuex 中的 mutation 非常类似于 事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) { // 变更状态
state.count++
}
}
})
不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:
//HelloWorld.vue
<button type="button" @click="store.commit('increment')">count is {{state.count}}</button>
当然,我们也可以用commit去装载一些数据: payload
Payload
你可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
// ..
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
//store/index.js
...
mutations:{
increment(state,payload={amount:1}){
return (state.count =state.count+payload.amount)
},
},
...
//HelloWorld.vue
<button type="button" @click="store.commit('increment',{amount:10,})">count is {{state.count}}</button>
对象风格的提交方式
store.commit({
type: 'increment',
amount: 10
})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Mutation 必须是同步函数
在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
Actions
Actions存在的意义是假设你在修改state的时候有异步操作,vuex作者不希望我们将异步操作放在Mutations中,所以就给设置了一个区域,用来放异步操作,这就是Actions。
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
},
},
actions: { //可异步可同步
increment ({commit},payload) {
commit('increment',payload)
},
},
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。也可以根据 参数解构来简化代码(特别是我们需要调用 commit 很多次的时候):
actions: {
increment ({ commit }) {
commit('increment')
}
}
分发 Action
Action 通过 store.dispatch 方法触发:
//HelloWorld.vue
<button type="button" @click="store.dispatch('increment',{amount:20})">action handler is {{state.count}}</button>
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在组件中分发 Action
在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的。这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。 如果希望你的模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
computed: {
formatMessage() {
return this.message + 'world';
},
...mapState('cartModule', ['count']),
},
methods: {
...mapActions('cartModule', ['incrementIfOddOnRootSum']),
},
Vuex原理
如图,Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。围绕该生态圈,各模块在核心流程中的主要功能为:
Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。dispatch:操作行为触发方法,是唯一能执行action的方法。actions:操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。mutations:状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。
Pinia
简介
pinia 是由 vue 团队开发的,适用于 vue2 和 vue3 的状态管理库。
与 vue2 和 vue3 配套的状态管理库为 vuex3 和 vuex4,pinia被誉为 vuex5。 相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。
- pinia 没有命名空间模块。
- pinia 无需动态添加(底层通过
getCurrentInstance获取当前 vue 实例进行关联)。 - pinia 是平面结构(利于解构),没有嵌套,可以任意交叉组合。
安装
yarn add pinia --save
引入pinia
//src/main.ts
import {createApp} from 'vue'
const pinia = createPinia()
import App from './App.vue'
import { createPinia } from 'pinia'
creatApp(App).use(pinia).mount('#app')
Store 是什么?
Store (如 Pinia) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念: state、getter 和 action,可以假设这些概念相当于组件中的 data、 computed 和 methods。
应该在什么时候使用 Store?
一个 Store 应该包含可以在整个应用中访问的数据。
这包括在许多地方使用的数据 eg: 显示在导航栏中的用户信息,
以及需要通过页面保存的数据,eg: 一个非常复杂的多步骤表单。另一方面,应该避免在 Store 中引入那些原本可以在组件中保存的本地数据
eg: 一个元素在页面中的可见性。
并非所有的应用都需要访问全局状态,但如果你的应用确实需要一个全局状态,那 Pinia 可使你的开发过程更轻松。
定义Store
使用defineStore定义store,第一个参数必须是全局唯一的id,可以使用Symbol
import { defineStore } from 'pinia'
// 第一个参数必须是全局唯一
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 10 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
getters:{
double:(state) => state.count * 2,
}
actions: {
increment() {
return this.count++
},
},
})
然后就可以在一个组件中使用该 store 了:
//HelloWorld.vue
<script setup lang="ts">
import {useCounterStore} from '../store/index'
const counter =useCounterStore()
console.log(counter)
defineProps<{msg:string}>()
</script>
<template>
<div class="card">
<button type="button" @click="count++">count is {{counter.count}}</button>
</div>
</template>
使用和重置
<script setup lang="ts">
import { ref } from 'vue'
import {useCounterStore} from '../store/index.ts'
const counter = useCounterStore();
counter.count++
counter.increment()
// 重置
counter.$reset()
</script>
改变状态
counter.$patch({
count: counter.count + 1,
name: 'Abalam',
})
计算属性Getters
Getter 完全等同于 Store 状态的计算值,可以用 defineStore() 中的 getters 属性定义
export const useCounterStore = defineStore('counter', {
state: ():ICounterStoreState => ({
count: 0
}),
getters: {
doubleCount: state => state.count * 2
}
})
// 组建中可以直接使用
// counter.doubleCount
传递参数到getters
Getter 是计算属性,也可叫只读属性,因此不可能将任何参数传递给它们。但是可以从 getter 返回一个函数以接受任何参数。
import { defineStore } from 'pinia'
interface ICountStoreState {
count: number;
}
export const useCounterStore = defineStore('counter', {
state: ():ICounterStoreState => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
},
getters: {
doubleCount: state => state.count * 2,
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
}
})
动作Actions
Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。
export const useTodos = defineStore('todos', {
state: () => ({
todos: [],
filter: 'all',
// 类型将自动推断为 number
nextId: 0,
}),
getters: {
finishedTodos(state) {
return state.todos.filter((todo) => todo.isFinished)
},
unfinishedTodos(state) {
return state.todos.filter((todo) => !todo.isFinished)
},
filteredTodos(state) {
if (this.filter === 'finished') {
// 调用其他带有自动补全的 getters
return this.finishedTodos
} else if (this.filter === 'unfinished') {
return this.unfinishedTodos
}
return this.todos
},
},
actions: {
// 接受任何数量的参数,返回一个 Promise 或不返回
addTodo(text) {
// 你可以直接变更该状态
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})
访问其他 store 的 action
想要使用另一个 store 的话,直接在 action 中调用就好了:
export const useCounterStore = defineStore('counter', {
state: ():ICounterStoreState => ({
count: 0
}),
})
export const useSettingsStore = defineStore('settings', {
state: () => ({
preferences: null,
}),
actions: {
async fetchUserPreferences() {
const counter = useCounterStore()
console.log(counter)
},
},
})
Pinia 原理
- pinia中可以定义多个store,每个store都是一个reactive对象
- pinia的实现借助了scopeEffect
- 全局注册一个rootPinia,通过provide提供pinia
- 每个store使用都必须在setup中,因为这里才能inject到pinia
参考地址
pinia.vuejs.org/zh/getting-…
vuex.vuejs.org/zh/index.ht…