做前端几年下来,我始终有两个感觉:
- 我其实更习惯用“类 + 方法”来表达业务模型;
- 但在 React 里,主流状态管理方案(Redux / MobX / Zustand)要么过于 ceremony,要么在 IoC / 深度监听上不够顺手。
具体来说:
- Redux:生态强大,但需要 action / reducer / dispatch / selector,一写就是一堆模板代码,很多项目最后是为 Redux 的心智模型服务,而不是为业务服务。
- MobX:写起来很爽,但响应式系统比较“黑盒”,依赖收集和更新路径藏在内部;想做一些 IoC / 命名空间隔离 / 深度监听时,需要自己拼好多工具。
- Zustand:非常轻量好用,但本质还是一个“函数式 store”,在类模型、依赖注入、全局异步 loading 管理、深度 watch这些场景上,不是它的设计重点。
所以我想要一个东西:
能不能只用 TypeScript 的类来写业务模型,顺手就能:
- 在 React 里直接用 hooks 创建 / 注入模型实例;
- 按参数自动缓存实例,天然按业务 key 分区;
- 深度监听模型及其嵌套字段;
- 自带一个 IoC 容器和依赖注入能力;
- 还要有不错的性能。
于是就有了这个库:easy-model。
easy-model 是什么?
一句话:
围绕「类模型(Model Class)+ 依赖注入 + 精细化变更监听」构建的 React 状态管理与 IoC 工具集。
你可以用普通的 TypeScript 类描述业务模型,通过少量 API 即可:
- 在函数组件中直接创建 / 注入模型实例:
useModel/useInstance - 跨组件共享同一实例,支持按参数分组实例缓存:
provide - 监听模型及其嵌套属性的变化:
watch/useWatcher - 用装饰器和 IoC 容器做依赖注入:
Container/CInjection/VInjection/inject - 统一管理异步调用的加载状态:
loader/useLoader
npm 包:@e7w/easy-model
GitHub:https://github.com/ZYF93/easy-model
用法长什么样?
1)最基础的:类 + useModel + useWatcher
import { useModel, useWatcher } from "@e7w/easy-model";
class CounterModel {
count = 0;
label: string;
constructor(initial = 0, label = "计数器") {
this.count = initial;
this.label = label;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
}
function Counter() {
const counter = useModel(CounterModel, [0, "示例"]);
useWatcher(counter, (keys, prev, next) => {
console.log("changed:", keys.join("."), prev, "->", next);
});
return (
<div>
<h2>{counter.label}</h2>
<div>{counter.count}</div>
<button onClick={() => counter.decrement()}>-</button>
<button onClick={() => counter.increment()}>+</button>
</div>
);
}
- 状态就是字段(
count、label) - 业务就是方法(
increment/decrement) useModel负责在组件中创建并订阅实例useWatcher可以拿到变更路径 + 前后值
2)跨组件共享 + 参数分组:provide + useInstance
import { provide, useModel, useInstance } from "@e7w/easy-model";
class CommunicateModel {
constructor(public name: string) {}
value = 0;
random() {
this.value = Math.random();
}
}
const CommunicateProvider = provide(CommunicateModel);
function A() {
const { value, random } = useModel(CommunicateModel, ["channel"]);
return (
<div>
<span>组件 A:{value}</span>
<button onClick={random}>改变数值</button>
</div>
);
}
function B() {
const { value } = useInstance(CommunicateProvider("channel"));
return <div>组件 B:{value}</div>;
}
- 相同参数(
"channel")拿到的是同一个实例 - 不同参数会拿到不同实例
- 用起来不需要自己设计 Context / key-value 容器,
provide会帮你做。
3)React 外的深度监听:watch
import { provide, watch } from "@e7w/easy-model";
class WatchModel {
constructor(public name: string) {}
value = 0;
}
const WatchProvider = provide(WatchModel);
const inst = WatchProvider("watch-demo");
const stop = watch(inst, (keys, prev, next) => {
console.log(`${keys.join(".")}: ${prev} -> ${next}`);
});
inst.value += 1;
// 不需要时取消
stop();
- 可以在 React 组件外 做监听(例如日志、埋点、状态同步到别的系统)
keys是精确到字段路径的,如["child2", "value"]
4)统一管理异步加载状态:loader + useLoader
import { loader, useLoader, useModel } from "@e7w/easy-model";
class LoaderModel {
constructor(public name: string) {}
@loader.load(true)
async fetch() {
return new Promise<number>(resolve =>
setTimeout(() => resolve(42), 1000)
);
}
}
function LoaderDemo() {
const { isGlobalLoading, isLoading } = useLoader();
const inst = useModel(LoaderModel, ["loader-demo"]);
return (
<div>
<div>全局加载状态:{String(isGlobalLoading)}</div>
<div>当前加载状态:{String(isLoading(inst.fetch))}</div>
<button onClick={() => inst.fetch()} disabled={isGlobalLoading}>
触发一次异步加载
</button>
</div>
);
}
@loader.load(true)装饰器会把这个方法纳入“全局 loading”管理useLoader提供:isGlobalLoading:是否有任意一个被 loader 管理的方法在执行isLoading(fn):某个具体方法是否在执行
5)IoC 容器 + 依赖注入:Container / CInjection / VInjection / inject
import {
CInjection,
Container,
VInjection,
config,
inject,
} from "@e7w/easy-model";
import { object, number } from "zod";
const schema = object({ number: number() }).describe("测试用schema");
class Test {
xxx = 1;
}
class MFoo {
@inject(schema)
bar?: { number: number };
baz?: number;
}
config(
<Container>
<CInjection schema={schema} ctor={Test} />
<VInjection schema={schema} val={{ number: 100 }} />
</Container>
);
- 用 zod 的 schema 做“依赖描述”
- 在
Container里用:CInjection注入构造函数VInjection注入常量值
- 业务类里用
@inject(schema)直接拿到依赖
这一块更多是后期复杂项目 / 多模块协作时的味道,前期不用也没关系。
和 Redux / MobX / Zustand 的对比
从编程模型 / 心智负担 / 性能三个角度简单对比一下:
| 方案 | 编程模型 | 典型心智负担 | 内建 IoC / DI | 性能特征(本项目场景) |
|---|---|---|---|---|
| easy-model | 类模型 + Hooks + IoC | 写类 + 写方法即可,少量 API(provide / useModel / watch 等) | 是 | 在极端批量更新下仍为个位数毫秒 |
| Redux | 不可变 state + reducer | 需要 action / reducer / dispatch 等模板代码 | 否 | 在同场景下为数十毫秒级 |
| MobX | 可观察对象 + 装饰器 | 对响应式系统有一定学习成本,隐藏的依赖追踪 | 否(偏响应式而非 IoC) | 性能优于 Redux,但仍是十几毫秒级 |
| Zustand | Hooks store + 函数式更新 | API 简洁,偏轻量,适合局部状态 | 否 | 在本场景下是最快,但不提供 IoC 能力 |
项目视角下:
- 对比 Redux:
- 不拆 action / reducer / selector,业务逻辑直接写在类方法里;
- 模板代码大幅减少,类型推断更直接;
- 实例缓存 + 变更订阅都在 easy-model 内部处理,不用自己写 connect / useSelector。
- 对比 MobX:
- 一样是“类 + 装饰器”的感觉,但用更显式的 API(
watch/useWatcher)暴露依赖; - 内置 IoC / 命名空间 / clearNamespace,做服务注入、配置管理更顺手。
- 一样是“类 + 装饰器”的感觉,但用更显式的 API(
- 对比 Zustand:
- 性能接近(下面有 benchmark),但功能更偏向“中大型业务的领域建模 + IoC”,不是简单的局部状态 store 替代品。
简单 benchmark:极端场景下的粗略对比
我在 example/benchmark.tsx 里写了一个刻意极端、但容易复现的 benchmark,核心场景是:
- 初始化一个包含 10,000 个数字的数组;
- 点击按钮后,对所有元素做 5 轮自增;
- 使用
performance.now()统计这段同步计算与状态写入时间; - 不计入 React 首屏渲染时间,只看每次点击带来的计算 + 写入耗时。
在一台普通开发机上的一次测试(取单次代表值,大致水平):
| 实现 | 耗时(ms) |
|---|---|
| easy-model | ≈ 3.1 |
| Redux | ≈ 51.5 |
| MobX | ≈ 16.9 |
| Zustand | ≈ 0.6 |
说明几点:
- 这是一个刻意放大的“批量更新”场景,主要用来放大架构上的差异;
- 结果受浏览器 / Node、硬件、打包模式等影响,只能作为趋势参考;
- Zustand 在这个场景下最快,符合它“极简 store + 函数式更新”的定位;
- easy-model 虽然比不上 Zustand,但仍然明显快于 Redux / MobX,同时换来:
- 类模型 + IoC + 深度监听等高级能力;
- 更适合中大型项目的结构化建模体验。
如果你感兴趣,欢迎直接把仓库拉下来,在 example/benchmark.tsx 里自己跑一跑。
适合什么项目用 easy-model?
我个人觉得,easy-model 比较适合下面几类场景:
- 有比较清晰的领域模型,希望用类来承载状态和方法;
- 项目里有不少“服务 / 仓储 / 配置 / SDK 封装”之类的抽象,希望用 IoC 管理;
- 对“监听某个模型 / 某个嵌套字段的变化”有刚需(比如审计、埋点、状态镜像等);
- 想在保证结构化 & 可维护性的前提下,拿到接近轻量状态库的性能。
不太适合的情况:
- 只是非常简单的小组件状态,用 Zustand 这种“hooks store”会更轻;
- 团队对装饰器 / IoC 天然排斥,或者项目不方便启用对应 TS / Babel 配置。
未来准备做的几件事
也坦白说一下目前的不足,和打算补的方向:
- DevTools:希望做一个简单的可视化面板,把
watch的变更、模型树、时间线跑出来。 - 更细粒度的订阅能力:在 React 渲染维度做更精细的 selector / 局部订阅,进一步减少不必要重渲染。
- SSR / Next.js / React Native 等场景的最佳实践:把现在脑子里的用法沉淀成文档或示例仓库。
- 模板项目 / 脚手架:降低上手门槛,不用大家一上来就先调 tsconfig / Babel / decorator 配置。
如果你在实际项目中踩到什么坑,或者有什么“这块能不能做得更爽一点”的想法,也欢迎在 GitHub 提 issue 或 PR。
最后
如果你:
- 正在用 Redux / MobX / Zustand 做中大型项目;
- 觉得模板代码有点多、心智模型有点绕;
- 又习惯用“类 + 方法”去表达业务逻辑;
不妨试试 easy-model,先把其中一个模块迁过来感受一下:
- GitHub:
https://github.com/ZYF93/easy-model - npm:
@e7w/easy-model
欢迎 Star、试用、吐槽、提 issue,一起把这个“类模型 + IoC + watch”的方向打磨得更好一点。