从零手写一个状态管理库
一、先明确它要解决什么问题
状态管理要解决的是跨组件共享和变更通知这两件事。
很多项目用状态管理库,实际只用到了它百分之十的功能:一个全局变量 + 一个订阅列表。理解了这一层,就没必要一上来引依赖。
二、核心就三块
1. 存和取
用一个普通对象存状态。约定 key 用 $ 开头存内部状态,这样和业务数据不会混在一起(业务可能会存一个叫 user 的东西)。
const globalState = { $count: 0, $name: '未命名' };
2. 订阅
谁关心这个 key,谁就把回调记下来。
const watchers = new Map();
function watch(key, callback) {
if (!watchers.has(key)) watchers.set(key, new Set());
watchers.get(key).add(callback);
return () => watchers.get(key).delete(callback);
}
返回一个取消订阅的函数——这一点很重要,否则组件卸载后回调还在,之后会一直报错。Vue 的组合式函数基本都是这么设计的。
3. 通知
值真的变了才通知。这里是最容易写错的地方。
function set(key, value) {
const old = globalState[key];
if (old === value) return; // 相等就不通知,避免无谓的渲染
globalState[key] = value;
const list = watchers.get(key);
if (list) list.forEach((cb) => cb(value, old));
}
old === value 这个判断必须加。不加的话,每次设置成相同的值都会触发一遍所有回调,性能和正确性都有问题(尤其是对象类型,虽然对象的引用变了内容可能没变)。
三、派生值
有些值是从别的值算出来的,比如总价 = 单价 × 数量。这类值不该单独存,存了就会出现两边不一致。
function computed(getter, keys) {
return watch(keys, () => { /* 依赖变了触发 */ });
}
// 用法
const total = computed(
() => state.price * state.count,
['price', 'count']
);
更实用的做法是直接写成函数,每次读取时现算。少几行代码,也不会出现同步问题:
function getTotal() {
return state.price * state.count;
}
在组件里用它就行。性能问题的答案是"慢的话再加 memo",不是一开始就上复杂机制。
四、和组件对接
在前端框架里,大致是这样接的:
function useStore() {
const snapshot = ref({ ...globalState });
function set(key, value) {
storeSet(key, value);
snapshot.value = { ...globalState }; // 触发视图更新
}
return { state: snapshot, set };
}
这里用「每次变更整体替换一次引用」的方式来触发更新,简单粗暴但可靠。真正精细的依赖收集(只更新用到的部分)实现起来复杂度高不少,收益在小项目里看不出来。
五、这套实现的边界
能用的地方: 主题、表单草稿、当前用户这类少量全局数据。
不够用的地方:
- 异步状态。 接口请求要有 loading 和 error 三个状态,自己维护很容易乱。
- 状态之间的依赖。 A 变了 B 要跟着变,依赖一多就变成了隐式耦合。
- 调试。 状态分散在多个回调里,出问题时数据从哪来的很难追。
- 时间旅行。 出错时想回到上一步看看,这种需求这套实现给不了。
六、什么时候该换回库
判断标准是状态之间的关系复杂度,不是项目大小:
- 只有几个互不相关的全局字段 → 自己写够了
- 状态之间有派生关系、有异步、有多个组件同时读写 → 上成熟库
我发现大多数项目一开始自己写,写到第三层嵌套就明白了为什么要用库。这个过程本身是有价值的,因为用库之后你才知道它帮你省掉了哪部分。
- 点赞
- 收藏
- 关注作者
评论(0)