Vuejs状态管理

1,304 阅读7分钟

为什么需要状态管理

状态自管理应用包含以下几个部分:

  • 状态(Action),驱动应用的数据源;
  • 视图(View),以声明方式将状态映射到视图;
  • 操作(State),响应在视图上的用户输入导致的状态变化。

“单向数据流”理念的简单示意:

image.png

Vue 最重要就是 数据驱动组件化,每个组件都有自己 data ,templatemethods, data是数据,我们也叫做状态,通过methods中方法改变状态来更新视图,在单个组件中修改状态更新视图是很方便的,但是实际开发中是多个组件(还有多层组件嵌套)共享同一个状态
兄弟组件需要通信,这个时候传参就会很繁琐,就需要进行状态管理,负责组件中的通信,方便维护代码。

image.png

需要注意的点:

  • 改变状态的唯一途径就是提交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.statecontext.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)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割:

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 的状态

模块的局部状态

对于模块内部的 mutationgetter,接收的第一个参数是模块的局部状态对象。

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
    }
  }
}

命名空间

默认情况下,模块内部的 actionmutation 仍然是注册在全局命名空间的。这样使得多个模块能够对同一个 actionmutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。 如果希望你的模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。

computed: {
    formatMessage() {
      return this.message + 'world';
    },
    ...mapState('cartModule', ['count']),
},
methods: {
    ...mapActions('cartModule', ['incrementIfOddOnRootSum']),
},

Vuex原理

image.png 如图,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 componentsdata对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
  • gettersstate对象读取方法。图中没有单独列出该模块,应该被包含在了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) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念: stategetteraction,可以假设这些概念相当于组件中的 datacomputedmethods

应该在什么时候使用 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 原理

  1. pinia中可以定义多个store,每个store都是一个reactive对象
  2. pinia的实现借助了scopeEffect
  3. 全局注册一个rootPinia,通过provide提供pinia
  4. 每个store使用都必须在setup中,因为这里才能inject到pinia

参考地址
pinia.vuejs.org/zh/getting-… vuex.vuejs.org/zh/index.ht…