5月27日 18:11

MobX 中 makeObservable、makeAutoObservable 和装饰器有什么区别?

三者的核心区别在于声明方式:makeObservable 需要显式标注每个成员的类型,makeAutoObservable 自动推断成员类型,装饰器用 @ 语法标记但需要编译器支持。

MobX 6 之后官方推荐函数式 API(makeObservable / makeAutoObservable),装饰器变为可选项。传统装饰器(legacy decorators)永远不会成为 JS 标准的一部分,MobX 7 将移除对它们的支持。如果你还在用 @observable 写法,迁移计划该提上日程了。

makeObservable:精确控制每个属性

javascript
class TodoStore { todos = []; loading = false; constructor() { makeObservable(this, { todos: observable.shallow, // 浅层观察,数组引用变才触发 loading: observable, unfinishedCount: computed, addTodo: action, fetchTodos: flow // flow 处理 async/await }); } get unfinishedCount() { return this.todos.filter(t => !t.done).length; } addTodo(text) { this.todos.push({ text, done: false }); } *fetchTodos() { this.loading = true; try { const res = yield fetch("/api/todos"); this.todos = yield res.json(); } finally { this.loading = false; } } }

makeObservable 最大的价值是精细控制。observable.shallow 只观察引用变化,数组内部对象的改动不会触发响应——这在列表渲染场景下能避免大量不必要的 re-render。observable.ref 只观察赋值,不做深度转换,适合存不可变数据。flow 专门标注 generator 函数处理异步流程,自动管理 pending/error 状态。

缺点也明显:每个属性都要手动标注,漏写一个就丢响应性,而且这类 bug 不会报错,只是默默不更新。

makeAutoObservable:自动推断,省心省力

javascript
class TodoStore { todos = []; loading = false; constructor() { makeAutoObservable(this); } get unfinishedCount() { return this.todos.filter(t => !t.done).length; } addTodo(text) { this.todos.push({ text, done: false }); } }

推断规则很直接:字段 → observable,getter → computed,方法 → action。一个 makeAutoObservable(this) 就完事。

如果某个成员不想被自动推断,可以覆盖:

javascript
constructor() { makeAutoObservable(this, { todos: observable.shallow, // 覆盖:用浅层观察 helper: false, // 排除:不使其可观察 fetchTodos: flow // 覆盖:generator 用 flow }); }

_ 开头的属性默认不会被自动推断,这是 MobX 的约定。如果你有内部辅助字段不想暴露为响应式,加个下划线前缀就行。

注意makeAutoObservable 不能用在有超类的类上。子类继承时会报错,因为自动推断无法正确处理继承链上的属性。这种场景必须用 makeObservable

装饰器:语法糖,有前提条件

javascript
class TodoStore { @observable todos = []; @observable loading = false; @computed get unfinishedCount() { return this.todos.filter(t => !t.done).length; } @action addTodo(text) { this.todos.push({ text, done: false }); } }

装饰器写法最直观,属性和类型标注在一起,读起来很清晰。但有两个前提条件经常被忽略:

  1. 必须配置编译器。TypeScript 需要在 tsconfig.json 中启用 experimentalDecorators,Babel 需要 @babel/plugin-proposal-decorators。没配对就报错,配错了行为也可能不一致。
  2. 传统装饰器 vs 标准装饰器。MobX 6 同时支持两种,但行为不同。传统装饰器(legacy)用 @observable x = value,标准装饰器(Stage 3)用 @observable accessor x = value。2023 年之后 TC39 确定的标准写法是后者,传统写法已被废弃。

另外,用了装饰器不代表可以省掉 makeObservable。MobX 6 中,即使类上写了 @observable,构造函数里还是得调用 makeObservable(this),否则装饰器不生效。这一点很多人踩坑。

怎么选?

场景推荐原因
新项目,没有装饰器依赖makeAutoObservable最少代码,自动推断
需要浅层观察或排除某些属性makeAutoObservable + 覆盖覆盖写法比全手动省事
有继承关系的 StoremakeObservablemakeAutoObservable 不支持继承
需要 observable.shallow / observable.refmakeObservable精细控制每个属性
项目已有装饰器配置,团队习惯装饰器 + makeObservable(this)不用为了迁移而迁移

一句话:默认用 makeAutoObservable,碰到继承或需要精细控制时换 makeObservable,装饰器只在已有项目依赖时继续用。

追问

makeAutoObservable 和 makeObservable 可以混用吗?

不行。一个类里只能选一个。但 makeAutoObservable 的第二个参数本身就是覆盖写法,本质上就是 makeAutoObservable + 部分手动标注的混合体。

装饰器写的老项目怎么迁移到 makeAutoObservable?

分两步:先把 @observable / @computed / @action 标注转为 makeObservable(this, {...}) 的写法,确认行为一致后,再考虑能否简化为 makeAutoObservable。迁移过程中最容易漏的是 makeObservable(this) 这个调用——老代码用了装饰器但忘记在构造函数里调用它,迁移时同样容易忘。

observable.shallow 和 observable 有什么区别?

observable 会递归地把对象内部所有嵌套属性都变成可观察的,observable.shallow 只观察第一层引用。对于数组,observable.shallow 只在数组引用变化时触发响应,数组内部元素的属性变化不会触发。列表渲染场景用 observable.shallow 能显著减少不必要的更新。

为什么 makeAutoObservable 不支持继承?

因为自动推断在遍历 this 上的所有属性时,无法区分哪些是从父类继承的、哪些是子类自己的。父类可能已经对自己的属性做了 makeAutoObservable,子类再调一次就会重复处理。所以 MobX 直接禁止了这种用法,有继承需求的必须用 makeObservable 显式标注。

写段代码

javascript
// 实际项目中常见的模式: // 基类用 makeObservable,子类也用 makeObservable + override class BaseStore { loading = false; constructor() { makeObservable(this, { loading: observable, }); } } class TodoStore extends BaseStore { todos = []; constructor() { super(); makeObservable(this, { loading: override, // 继承的属性用 override todos: observable.shallow, addTodo: action.bound, // 自动绑定 this }); } addTodo(text) { this.todos.push({ text, done: false }); } }
标签:Mobx