你不需要Vuex

5,285 阅读8分钟

先来说说为什么

  1. 使用Vuex会使代码变得繁琐冗余(代码太多了)“被忽略的官方说明”,而大多时候我们并没有用到所谓的状态追踪,只是简单的进行gettersettermutations;当状态多的时候,Vuex真的可以有利于我们对代码便捷的阅读吗?

  2. 不利于编辑器和typescript中代码查找,每次要找某个变量的时候,要在store文件全局搜,各种代码片段去反复跳;现在这种状态管理方式解决了这个痛点:像一些标准的多语言编辑器vscodeatom等是可以检查到静态代码的,Ctrl+点击可以跳到对应位置、鼠标放上去某个字段时,会得到定义时声明的类型提示和代码片段(js中需要用jsDoc注释声明),一开始我也觉得这东西没用,后面写过typescript才发现,原来javascript也一样可以通过jsDoc注释来提供typescript一样的类型提示,真香。

设计模式

这里我推荐自行定义class作为分模块数据管理会更加好,理由就是数据庞大的时候可以拆分为各class来便于对状态的管理,为什么不使用普通object单例对象?因为普通object没有继承、没有自身调用的构造函数,在ts中类型定义会比较麻烦,而且没有class中好用的privateprotectedclass可以作为interface使用等;一句话总结就是:“没有class灵活”,这个我也是从游戏编程那边借鉴来的习惯。

写这篇文章的时候,我早就在2017年开始实际项目中使用class替代vuex了,所以并不用担心有其他问题,而且Vue 3.x中也可以复用,而且这只是一个编程的设计模式,并不是我个人发明的方言

先来看下以下代码

const a = {
    data: {
        value: 10
    }
}
const b = {
    data: a.data
};

b.data.value = 20;

console.log(a, b); // 输出 {data: { value: 20 }}, {data: { value: 20 }}

先来说下原理:因为javascript变量对等赋值的时候,指针指向同一个内存,所以依赖这个特性和Vue中赋值是直接对等,就可以自行定义一些全局的状态属性,依次注入到需要同步更新的组件中。废话不多说,直接看代码结构:

step 1 src/store/index.js

// 自行定义一个class作为数据管理
class ModuleStore {

    /** 订单信息 */
    orderInfo = {
        /** 订单名 */
        name: "订单" + Math.random().toString(36).substr(2),
        /** 订单日期 */
        date: "2018/12/12 12:12:12",
        /** 
         * 订单状态 
         * @type {"ok"|"fail"|"invalid"|"wait"} 完成 | 失败 | 无效 | 待支付
        */
        state: "ok"
    }

}

/** 状态管理模块 */
const store = new ModuleStore;

export default store;

step 2 src/goods.vue

<script>
import store from "../store";
export default {
    data () {
        return {
            // 当前组件响应式用到的数据,这里对等之后,修改 store.orderInfo 的值就是响应式了
            pageData: store.orderInfo
        }
    }
}
</script>

step 3 src/list.vue

<script>
import store from "../store";
export default {
    data () {
        return {
            // 当前组件响应式用到的数据,这里对等之后,修改 store.orderInfo 的值就是响应式了
            listData: store.orderInfo
        }
    },
    methods: {
        modifyState() {
            // 这里修改了,其他引用到 store.orderInfo 所有组件都会同步修改
            this.listData.state = "wait";
            // 或者
            store.orderInfo.state = "wait";
        }
    }
}
</script>

注意事项

<script>
import store from "../store";
export default {
    ...more,
    methods: {
        modifyState() {
            // 注意不要这样去修改整个属性,因为会导致对象内所有的属性都失去了指针(参照一开始那个段代码片段)
            this.listData = {
            	name: "修改订单名",
                date: new Date().toLocaleString()
            }
            // 但是我不想
            // this.listData.name = "修改订单名";
            // this.listData.date = new Date().toLocaleString()
            // 咋整?
        }
    }
}
</script>

进阶一下状态模块设计

  1. 创建一个ModifyObject.js,可以分代码多方式去使用:默认导出给其他模块继承使用作为普通单例工具使用;在typescript中这两个方法我会用泛型去约束传参,那么就确保了状态字段的可靠性。
// 因为可能存在多个状态模块,所以定义一个基类,导出让其他模块继承使用
// 这里 export 是给其他模块继承用
export class ModuleModifyObject {

    /**
     * 修改属性值-只修改之前存在的值
     * @param {object} target 修改的目标
     * @param {object} value 修改的内容
     */
    modifyData(target, value) {
        for (const key in value) {
            if (Object.prototype.hasOwnProperty.call(target, key)) {
                target[key] = value[key];
            }
        }
    }
	
    /**
     * 设置属性值-之前不存在的值也根据传入的`value`值去设置
     * @param {object} target 设置的目标
     * @param {object} value 设置的内容
     */
    setData(target, value) {
        for (const key in object) {
            target[key] = value[key];
        }
    }
}

const modifyObject = new ModuleModifyObject();

// 这里 export default 是作为单例用
export default modifyObject;
  1. 再来改造下src/store/index.js
import {
    ModuleModifyObject
} from "./utils/ModifyObject"

class ModuleStore extends ModuleModifyObject {
    ...more
}
...more
  1. 现在回到src/list.vue
<script>
import store from "../store";
export default {
    ...more,
    methods: {
        modifyState() {
            const obj = {
                name: "修改订单名",
                date: new Date().toLocaleString(),
                // 下面是不在 store.orderInfo 的属性,上面的基类中我已经做了处理,所以本身不存在的属性是不会更改到的
                a: "php",
                b: "java"
            }
            
            // 写法一
            store.modifyData(this.listData, obj); 
            // 写法二
            store.modifyData(store.orderInfo, obj);
       	    // 这样就不需要 this.listData.name = "修改订单名"; this.listData.date = new Date().toLocaleString() 这种逐个属性去修改了
        }
    }
}
</script>

真正意义上做到不能直接修改store.xxx,而是通过mutations去修改状态

1. 开始改用typescript,首先要定义两个核心类型工具,一会用到

/** 深层递归所有属性为可选 */
export type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
}

/** 深层递归所有属性为只读 */
export type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
}

2. 改造一下store模块;注意这里不是要使用vuexmutations,而是通过对应方法去约束使用

interface UrderInfoType {
    /** 用户名 */
    name: string
    /** 用户出生日期 */
    date: string
    /** 性别 */
    gender: "man" | "woman"| ""
    /** 登录信息 */
    api: {
        /** 登录凭据 */
        token: string
        /** 登录`id` */
        id: string
    }
}

class ModuleStore extends ModuleModifyObject {

    /** 用户信息 */
    readonly userInfo: DeepReadonly<UrderInfoType> = {
        name: "",
        date: "",
        gender: "",
        api: {
            token: "",
            id: "",
        }
    }

    /**
     * 更新用户信息,并缓存到`sessionStorage`中
     * @param value 要更新的值
     */
    updateUserInfo(value: DeepPartial<UserInfoType>) {
        // `modifyData`方法在里面做了递归处理,可以看下面的代码仓库有写,这里不做解释
        this.modifyData(this.userInfo, value);
        // or
        // this.modifyData(this.userInfo as DeepPartial<UserInfoType>, value);
        sessionStorage.setItem("user-info", JSON.stringify(this.userInfo));
    }
}

3. 回到使用场景

<script>
import store from "../store";
export default {
    data () {
        return {
            // 挂载响应式状态
            userInfo: store.userInfo
        }
    },
    methods: {
    	setToken() {
            // 现在不能直接修改了,编辑器会报错提示:无法分配到 "token" ,因为它是只读属性。
            // this.userInfo.api.token = ""
            
            // 现在只能通过`updateUserInfo`去更改要更改的属性,而且有类型约束和代码提示,再也不会担心状态出错,
            // 反正我是不会去记住每一个属性的,这些就应该交给机器去记住然后提示出来
            store.updateUserInfo({
            	api: {
                    token: "adfsdf2sd4f5s4d"
                }
            })
        }
    }
}
</script>

4. 对比下vuex中的mutations

const store = new Vuex.Store({
    state: {
        userInfo: {
            ...more 
        }
    },
    mutations: {
    	updateUserInfo(state, value) {
            state.userInfo = value;
            sessionStorage.setItem("user-info", JSON.stringify(state.userInfo));
        }
    }
})

// 使用时,官方给出的例子是:如果不想覆盖旧属性的值去改某个属性,那么这样写

this.$store.commit(updateUserInfo, {
    ...this.$store.state.userInfo,
    ...{
    	api: { 
        	token: "asfdf45s4d54s5d4f5",
            id: vuexStore.state.userInfo.api.id
        }
    }
})

// 实在太丑陋了,而且属性多的时候(对象属性很深层),不一定能保证你可以百分百没写错,懂我要说什么吧?
// 或者说,你在外面设置好修改之后的数据再传进去不就好了吗?那来看看是怎么样的

const value = JSON.parse(JSON.stringify(this.$store.state.userInfo)); // 这里必须要深拷贝,不然直接修改到状态了
value.api.token = "asfdf45s4d54s5d4f5"
this.$store.commit(updateUserInfo, value);

// 通过对比,应该能看出来有什么区别了

以上这个就是基本的状态管理实现,说下我在项目中用到的状态监听处理:在ModuleStore这个类里面使用Object.defineProperty或者new Proxy作为更复杂的操作,自定义的class作为状态管理更容易理解,而且扩展性也高。

最后对比优缺点,优点:代码编辑器(以vscode为例)静态代码追踪提示非常友好(Vuex无法实现,而且代码多的时候找某个属性太费力了,所以我才放弃使用),数据庞大时尤其明显,如果你是使用ts,配合 readonlyprivateenum 等关键字使用,可维护、阅读性简直再舒服不过。缺点:无法使用浏览器的vuex插件,不过有了静态代码分析检测,也不需要调试插件了。

Vue 3.x 同样可以复用

稍作改动,在store模块中把对应的属性加上reactive()即可,下面代码中,取消了上面使用的继承操作,而是把modifyDatasetData单独抽离了出来;

新建一个状态模块/store/Order.ts

import { reactive } from "vue";
import { jsonParse, modifyData } from "@/utils";

interface OrderInfo {
  id: number | ""
  /** 超时判断的时间 */
  time: string | number
  /** 商品属性对象 */
  goods: {
    id: number | ""
    name: string
    /**
     * 商品价格
     * - 前端展示时需要除以`100`
     */
    price: number
  }
  /**
   * 生成订单时间
   * - 当`new Date(time).getTime() > new Date(createTime).getTime()`时,当前订单则为超时,执行清空操作
   */
  createTime: string | number
}

function useOrderInfo(): DeepReadonly<OrderInfo> {
  return {
    id: "",
    time: "",
    goods: {
      id: "",
      name: "",
      price: 0
    },
    createTime: ""
  }
}

const key = "ModuleOrder";

export default class ModuleOrder {
  constructor() {
    this.init();
  }
  
  /** 订单信息 */
  readonly info = reactive(useOrderInfo())

  /** 初始化缓存信息 */
  private init() {
    const value = jsonParse(sessionStorage.getItem(key));
    modifyData(this.info, value);
  }

  /**
   * 更新 & 设置状态数据
   * @param value 
   */
  update(value: DeepPartial<OrderInfo>) {
    modifyData(this.info, value);
    sessionStorage.setItem(key, JSON.stringify(this.info));
  }

  /** 重置 */
  reset() {
    modifyData(this.info, useOrderInfo());
    sessionStorage.removeItem(key);
  }

}

导入到整体store模块下 /store/index.ts

import ModuleUser from "./User"; // 与 Order 一样的操作文件
import ModuleOrder from "./Order";

class ModuleStore {
  
  /** 用户状态模块 */
  readonly user = new ModuleUser();

  /** 订单状态 */
  readonly order = new ModuleOrder();

}

const store = new ModuleStore();

export default store;

页面应用 /views/demo.vue

<template>
  <div>
    <div>{{ userInfo.name }}</div>
    <span class="the-tag cyan">商品名称:{{ orderInfo.goods.name }}</span>
    <span class="the-tag red">${{ orderInfo.goods.price / 100 }}</span>
    <button @click="onEditPrice()">修改价格</button>
    <button @click="onSubmit()">发起支付</button>
  </div>
</template>
<script lang="ts" setup>
import store from "@/store";

const userInfo = store.user.info;

const orderInfo = store.order.info;

function onEditPrice() {
  store.order.update({
    goods: {
      price: 2199
    }
  })
}

function onSubmit() {
  if (new Date(orderInfo.time).getTime() > new Date(orderInfo.createTime).getTime()) {
    alert(`当前用户【${userInfo.name}】未在${orderInfo.time}之前下单,请重新操作`);
    store.order.reset();
    return;
  }
}
</script>

可以看到,vue 3.x之后提供了reactive()这个api使得响应式变量可以直接提取到其他地方去声明,相比vue 2.x的操作简单了一些,代码更直观一点;以上这个设计模式在两者中,实现的功能都是相同的。

最后附上使用到的项目:vue-admin-templateuni-app-template

有问题欢迎提出~