一、概述

Vuex >> 是一个专为 Vue.js 应用程序开发的 状态管理模式 + 库。它采用 集中式 存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

1. 什么是状态管理?

让我们从一个简单的 Vue 计数应用开始:

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. const state = reactive({
  4. count: 0 /** 状态 */,
  5. });
  6. const increment = () => {
  7. state.count++; /** 操作 */
  8. };
  9. </script>
  10. <!-- 视图 -->
  11. <template>
  12. <div>{{ state.count }}</div>
  13. </template>

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

  • 状态,驱动应用的数据源(state.count);
  • 视图,以声明方式将 状态 映射到视图(模板渲染);
  • 操作,响应在 视图 上的用户输入导致的状态变化(点击按钮)。

以下是一个表示 “单向数据流” 理念的简单示意:

flow.png

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

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。

对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。

以上的这些模式非常脆弱,通常会导致无法维护的代码。Vuex 的存在,就是为了解决类似问题,Vuex将组件的共享状态抽取出来,以一个全局 单例模式 管理,在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

vuex.png

流程:View(Dispatch)Action(Comit)Mutations(Mutate)StateView

解读:当用户在视图层进行某种操作需要修改数据时,通过派发(Dispatch)一个 Action,然后再 Action 内部提交(Commit)一次 Mutation,并在 Mutation 中执行数据更改,最终重渲染到视图上。

注意:Action 不是必需品,如果有异步操做才可能用到 Action,否则可以不使用。

2. 什么情况下使用 Vuex?

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:

Flux 架构就像眼镜:您自会知道什么时候需要它。

二、准备工作

1. 创建文件

  1. # Windows
  2. $ mkdir -p src/store; cd > src/store/index.ts
  3. # macOS
  4. $ mkdir -p src/store; touch src/store/index.ts

基本结构src/store/index.ts

  1. import { InjectionKey } from 'vue';
  2. import { createStore, Store, useStore as _useStore } from 'vuex';
  3. // → 为 store state 声明类型
  4. export interface IState {}
  5. // → 定义 injection key
  6. export const key: InjectionKey<Store<IState>> = Symbol();
  7. // → 构造 store
  8. export const store = createStore<IState>({
  9. state: {},
  10. getters: {},
  11. mutations: {},
  12. actions: {},
  13. });
  14. // → 自定义 useStore 钩子
  15. export const useStore = () => {
  16. return _useStore(key);
  17. };

2. 安装依赖

  1. $ npm install vuex@next

3. 注入

src/main.ts

  1. import { store, key } from './store';
  2. const app = createApp(App);
  3. // → 注入路由
  4. app.use(store, key);
  5. app.mount('#app');

三、核心概念

1. State *

1.1. 单一状态树

Vuex 使用 单一状态树(单例设计模式),用一个对象就包含了全部的应用层级状态。至此它便作为一个 唯一数据源 而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

1.2. 定义状态属性

我们定义一个状态属性 count,如下所示:

  1. state: {
  2. count: 0
  3. },

由于我们使用的是 TypeScript,所以别忘了声明 State 类型:

  1. // → 为 store state 声明类型
  2. export interface IState {
  3. count: number;
  4. }

提示:每当你新增一个状态属性,都应该在 IState 中为其添加相应的类型声明。

1.3. 在组件中访问状态属性

  1. <script setup lang="ts">
  2. // → 导入自定义钩子函数
  3. import { useStore } from '../store';
  4. // → 获取 store 对象
  5. const store = useStore();
  6. </script>
  7. <template>
  8. <!-- 访问 store 属性 -->
  9. <div>{{ store.state.count }}</div>
  10. </template>

2. Getters

Vuex 允许我们在 store 中定义 Getter(可以认为是 store 的计算属性)。就像计算属性一样,Getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

现在,我们在 state 中新增一个属性 idNo 用于记录用户的身份证号。

  1. export interface IState {
  2. count: number;
  3. idNo: string; /** 记录用户身份号 */
  4. }

然后在 state 中赋一个初始值:

  1. state: {
  2. count: 0,
  3. idNo: '51321198807168888',
  4. }

接下来,我们在 getters 字段下定义一个方法 birth,该方法根据 idNo 计算出出生年月,如下所示:

  1. getters: {
  2. birth: (state) => {
  3. let idNo = state.idNo;
  4. let year = idNo.slice(6, 10);
  5. let month = idNo.slice(10, 12);
  6. let day = idNo.slice(12, 14);
  7. return `出生年月:${year}-${month}-${day}`
  8. }
  9. },

最后,在组建中访问 Getters

  1. <div>{{ store.getters.birth }}</div>

3. Mutations *

更改 Vuex 的 store 中的状态的 唯一方法 是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型(函数名) 和一个 回调函数 ,值得注意的是,该函数只能是 同步函数。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

  1. mutations: {
  2. // → 更新state
  3. updateCount(state, payload: number) {
  4. state.count = payload;
  5. },
  6. // → 更新idNo
  7. updateIdNo(state, payload: string) {
  8. state.idNo = payload;
  9. },
  10. },

提交修改:

  1. <script setup lang="ts">
  2. import { useStore } from '../store';
  3. // → 获取 store 对象
  4. const store = useStore();
  5. // → 事件处理函数
  6. const increment = () => {
  7. /** 通过 commit 提交 mutaion,触发 state 更新 */
  8. store.commit('updateCount', store.state.count + 1);
  9. };
  10. </script>
  11. <template>
  12. <!-- 访问 store 属性 -->
  13. <div>{{ store.getters.birth }}</div>
  14. <div>{{ store.state.count }}</div>
  15. <!-- 点击按钮,使 count 自增 -->
  16. <button type="button" @click="increment">INCREMENT</button>
  17. </template>

4. Actions *

Action 类似于 Mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
  1. actions: {
  2. async action_name(context, payload) {
  3. // → 解构上下文
  4. const { state, commit, getters } = context;
  5. // → 执行异步操作
  6. const data = await fetch('');
  7. // → 提交修改
  8. commit('mutation_name', data);
  9. },
  10. },

分发 Action:

  1. store.dispatch('action_name', 'xxx');

5. modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

  1. const moduleA = {
  2. state: { ... },
  3. mutations: { ... },
  4. actions: { ... },
  5. getters: { ... }
  6. }
  7. const moduleB = {
  8. state: { ... },
  9. mutations: { ... },
  10. actions: { ... },
  11. getters: { ... }
  12. }
  13. const store = new Vuex.Store({
  14. modules: {
  15. a: moduleA,
  16. b: moduleB
  17. }
  18. })
  19. store.state.a // -> moduleA 的状态
  20. store.state.b // -> moduleB 的状态

5.1. 模块的局部状态

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

  1. const moduleA = {
  2. state: { count: 0 },
  3. mutations: {
  4. increment (state) {
  5. // 这里的 state 对象是模块的局部状态
  6. state.count++
  7. }
  8. },
  9. getters: {
  10. doubleCount (state) {
  11. return state.count * 2
  12. }
  13. }
  14. }

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

  1. const moduleA = {
  2. // ...
  3. actions: {
  4. incrementIfOddOnRootSum ({ state, commit, rootState }) {
  5. if ((state.count + rootState.count) % 2 === 1) {
  6. commit('increment')
  7. }
  8. }
  9. }
  10. }

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

  1. const moduleA = {
  2. // ...
  3. getters: {
  4. sumWithRootCount (state, getters, rootState) {
  5. return state.count + rootState.count
  6. }
  7. }
  8. }

5.2. 命名空间

参考这里 >>

四、拓展

vuex会遇到一个尴尬的问题,就是当用户手动刷新页面之后状态会被清空,我们可以在页面刷新的时候将状态存本地,每次加载的时候再读取就是了,具体操作如下:

  1. // → main.js
  2. // → 页面进入:合并状态
  3. const localState = localStorage.getItem('LOCAL_STATE');
  4. if (localState) {
  5. console.log('合并Store...');
  6. store.replaceState(Object.assign(store.state, JSON.parse(localState)));
  7. }
  8. // → 页面刷新:存储状态
  9. window.addEventListener('beforeunload', () => {
  10. console.log('缓存Store...');
  11. localStorage.setItem('LOCAL_STATE', JSON.stringify(store.state));
  12. });