MobX 创建可观察状态:makeObservable、makeAutoObservable 与注解系统深度解析
2026/9/19 20:45:09 网站建设 项目流程

MobX 创建可观察状态:makeObservable、makeAutoObservable 与注解系统深度解析

【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx

本篇聚焦 MobX 中「把属性、对象、数组、Map 和 Set 变为可观察状态」这一核心能力,系统讲解makeObservablemakeAutoObservableobservable三种 API 的用法、推断规则、可用注解全集与已知限制,并结合 makeObservable 源码、自动注解推断实现 与 测试用例 印证底层行为。读完本文,你可以直接在项目中新建可观察 Store、选择正确的注解组合,并理解每条限制背后的源码成因。

核心概念:状态、action 与 computed 三种注解

在 MobX 中,属性、整个对象、数组、Map 和 Set 都可以被制成可观察状态。让对象变得可观察的基本手段,就是使用makeObservable为每个属性指定一个注解(annotation)。三种最核心的注解是:

  • observable:定义一个可追踪的、用于存储状态的字段;
  • action:把方法标记为会修改状态的 action;
  • computed:把 getter 标记为从状态派生新事实、并缓存其输出的计算属性。

理解这三种注解的职责划分,是掌握 MobX 可观察状态体系的基础:observable存状态,action改状态,computed派生状态。

makeObservable:为已有属性指定注解

用法签名:

  • makeObservable(target, annotations, options?)

该函数用于让已存在的对象属性变得可观察。任何 JavaScript 对象(包括类实例)都可以作为target传入。典型用法是在类的构造函数中调用makeObservable,第一个参数为thisannotations参数是一个把注解映射到各个成员的字典,只有被注解的成员会受到影响。

import { makeObservable, observable, computed, action, flow } from "mobx" class Doubler { value constructor(value) { makeObservable(this, { value: observable, double: computed, increment: action, fetch: flow }) this.value = value } get double() { return this.value * 2 } increment() { this.value++ } *fetch() { const response = yield fetch("/api/value") this.value = response.json() } }

两点重要的对象描述符行为需要牢记(这也是 safeDescriptors 配置 所控制的行为):

  • 所有被注解的字段都是 non-configurable(不可重新配置)的
  • 所有非 observable(即无状态)字段(actionflow)都是 non-writable(不可写)的

源码视角:makeObservable 如何工作

从 makeObservable 的实现 看,整个流程分两步:先通过asObservableObject(target, options)为目标建立ObservableObjectAdministration管理对象,再遍历annotations的每个 key 调用内部的make_逐一应用注解。make_函数(见 makeObservable.ts#L92-L122)有一个关键细节:它会从目标实例出发沿原型链向上逐级查找属性描述符while (source && source !== objectPrototype)),这意味着定义在原型上的 getter、方法都能被正确注解;若目标上根本不存在该 key,会直接抛出错误(die(1, ...))。这也解释了文档限制中「makeObservable只能注解本类定义中声明的属性」这一条——它注解的是已定义的属性,而不是凭空创建。

使用现代装饰器的等价写法

使用现代装饰器(2022-03 规范)时,无需在构造函数中调用makeObservable,类可以直接这样写。注意@observable装饰器应始终与accessor关键字配合使用:

import { observable, computed, action, flow } from "mobx" class Doubler { @observable accessor value constructor(value) { this.value = value } @computed get double() { return this.value * 2 } @action increment() { this.value++ } @flow *fetch() { const response = yield fetch("/api/value") this.value = response.json() } }

两种写法表达的是同一份注解语义,装饰器只是把注解声明移到了成员定义处。

makeAutoObservable:自动推断所有注解

用法签名:

  • makeAutoObservable(target, overrides?, options?)

makeAutoObservable可以理解为「加强版的makeObservable」,因为它默认会推断所有属性应使用的注解。你仍可以通过overrides参数用特定注解覆盖默认推断——特别是false可以用来把某个属性或方法完全排除在注解处理之外。

import { makeAutoObservable } from "mobx" function createDoubler(value) { return makeAutoObservable({ value, get double() { return this.value * 2 }, increment() { this.value++ } }) }

注意:类同样可以使用makeAutoObservable,上面的差异只是展示了 MobX 如何适配不同的编程风格(工厂函数 vs 类)。

推断规则

  • 所有自身(own)属性变为observable
  • 所有getter变为computed
  • 所有setter变为action
  • 所有普通函数变为autoAction
  • 所有生成器函数变为flow(注意:某些转译器配置下无法检测生成器函数,如果flow未按预期工作,请显式指定flow);
  • overrides中标记为false的成员将不被注解,例如用它来排除只读字段(如标识符)。

这些规则与源码完全对应:autoannotation.ts 的make_函数 中依次判断——descriptor.get存在则委托给computed.make_descriptor.set存在则包装为 action;位于原型上(source !== adm.target_)的函数中,isGenerator为真则委托给flow,否则委托给autoAction;其余情况一律走observable(或options.deep === false时的observableRef)。

autoAction是推断中最特殊的一种:它既不是纯 action 也不是纯 computed,而是在运行时根据调用上下文决定本次调用是作为派生(被追踪)还是动作(批量更新)执行。测试用例 "makeAutoObservable actions can be used for state updaters and state readers" 验证了这一点:同一个double()方法被autorun调用时作为派生被追踪,而addTwo()中对状态的多处修改则被正确批处理,事件序列为[2, 6]

源码视角:性能优化与子类限制

从 makeAutoObservable 的实现 可以看到两个关键行为:

  1. 推断结果缓存:首次调用时,它会把目标实例及其原型的所有 key 收集进一个Set,并以隐藏属性keysSymbolSymbol("mobx-keys"))缓存到原型上(makeObservable.ts#L69-L77)。后续实例化无需再遍历原型,这正是文档限制中「make(Auto)Observable必须无条件调用」的原因——无条件调用才能安全地复用缓存的推断结果;
  2. 子类检查:开发模式下,若目标不是普通对象且其原型也不是普通对象,直接抛出'makeAutoObservable' can only be used for classes that don't have a superclass(makeObservable.ts#L51-L58)。对应测试 确认了带父类的类调用makeAutoObservable会抛出该错误。因此makeAutoObservable不能用于有 super 的类或被子类化的类——这类场景请改用makeObservable

observable:函数式创建可观察结构

用法签名:

  • observable(source, overrides?, options?)
  • @observable accessor(字段装饰器)

observable注解也可以作为函数调用,一次性让整个对象变得可观察。source对象会被克隆,其所有成员都会以类似makeAutoObservable的方式变为可观察。同样可以传入overrides映射来指定特定成员的注解。

import { observable } from "mobx" const todosById = observable({ "TODO-123": { title: "find a decent task management system", done: false } }) todosById["TODO-456"] = { title: "close all tickets older than two weeks", done: true } const tags = observable(["high prio", "medium prio", "low prio"]) tags.push("prio: for fun")

与前面makeObservable的示例不同,observable支持向对象动态添加(和删除)字段。这使得observable非常适合动态键控对象、数组、Map 和 Set 等集合类型。

源码视角:按类型分发的工厂逻辑

createObservable 函数 的实现揭示了其分派逻辑:已可观察的值直接原样返回;普通对象走observable.objectArray.isArrayobservable.array;ES6Map/Set分别走observable.map/observable.set其他普通对象(即类实例)原样返回、不做转换;最后兜底是observable.box。具体工厂实现见 observableFactories,其中box创建ObservableValuearray创建可观察数组,object则是通过extendObservable在一个新建的动态可观察对象上复制属性。

可观察数组示例

下面的示例创建一个可观察数组,并用autorun观察它。使用 Map 和 Set 集合的方式类似:

import { observable, autorun } from "mobx" const todos = observable([ { title: "Spoil tea", completed: true }, { title: "Make coffee", completed: false } ]) autorun(() => { console.log( "Remaining:", todos .filter(todo => !todo.completed) .map(todo => todo.title) .join(", ") ) }) // Prints: 'Remaining: Make coffee' todos[0].completed = false // Prints: 'Remaining: Spoil tea, Make coffee' todos[2] = { title: "Take a nap", completed: false } // Prints: 'Remaining: Spoil tea, Make coffee, Take a nap' todos.shift() // Prints: 'Remaining: Make coffee, Take a nap'

可观察数组还附带几个实用函数(实现在 ObservableArray 类 中):

  • clear():移除数组中当前所有条目;
  • replace(newItems):用新条目替换数组中所有现有条目;
  • remove(value):按值从数组中移除单个条目,找到并移除时返回true

关键注意事项

提示:与 JavaScript 的一般情况相同,不要用可观察的普通对象来创建键控集合(例如存储从用户 UUID 到用户对象的映射),应使用 Map 代替。MobX 会积极缓存对象的描述符,如果属性名不稳定,这可能导致内存泄漏。

这一提示有直接的源码依据:ObservableObjectAdministration 顶部就定义了模块级的descriptorCacheconst descriptorCache = Object.create(null)),用于缓存属性描述符。当键名频繁变化(如随机 UUID)时,缓存会持续增长,因此动态键控数据应优先使用observable.map

说明:原始值和类实例永远不会被转换为可观察对象

由于原始值在 JavaScript 中不可变,MobX 无法把它们变成可观察对象(但可以将它们装箱)。虽然除库之外通常没有使用这个机制的必要。

类实例即使传入observable或赋值给observable属性,也永远不会被自动制成可观察。把类成员制成可观察被认为是类构造函数的职责(即由类自身调用makeObservable)。这一行为与上文 createObservable 中 "other object - ignore" 分支 完全一致。

提示:observable的克隆 vsmakeObservable的原地更新

make(Auto)Observableobservable的主要区别在于:前者修改你传入的原始对象,而observable会创建一个克隆并使其可观察。

observable会创建一个 Proxy 对象,以便在把对象用作动态查找表时能拦截未来的属性添加。如果你想变成可观察的对象具有常规结构、所有成员都预先可知,makeObservable往往是更清晰的 API,因为它保留了原始对象标识。

因此,在工厂函数中推荐使用make(Auto)Observable

可用注解全览

注解说明
observable
observable.deep
定义一个可追踪的、存储状态的字段。若可能,赋给observable的任何值都会根据其类型自动转换为(深)observableautoActionflow。只有普通对象、数组、Map、Set、函数、生成器函数可被转换。类实例等保持不变。
observable.refobservable类似,但只追踪重新赋值。被赋的值完全被忽略,不会被自动转换为observable/autoAction/flow。例如,当你打算在可观察字段中存储不可变数据时使用它。
observable.shallowobservable.ref类似但面向集合。任何被赋的集合都会被制成可观察,但集合自身的内容不会变成可观察。
observable.structobservable类似,但如果赋的值与当前值结构相等,则忽略该赋值。
action把方法标记为会修改状态的 action。更多细节参见 actions。不可写。
action.boundaction类似,但会把 action 绑定到实例,从而this始终被设置。不可写。
computed可用于 getter,将其声明为可缓存的派生值。更多细节参见 computeds。
computed.structcomputed类似,但如果重算后的结果与上次结果结构相等,则不通知观察者。
true推断最佳注解。更多细节参见 makeAutoObservable。
false明确不对该属性进行注解。
flow创建一个flow来管理异步流程。更多细节参见 flow。注意 TypeScript 中推断的返回类型可能不准确。不可写。
flow.boundflow类似,但会把 flow 绑定到实例,从而this始终被设置。不可写。
override适用于子类覆盖父类的actionflowcomputedaction.bound
autoAction不应显式使用,它是makeAutoObservable在底层用来标记「可以既作为 action 又作为派生」的方法的注解,运行时会按调用上下文判定该函数本次是派生还是 action。

这些注解的底层差异体现在增强器(enhancer)上:modifiers.ts 定义了deepEnhancer(默认,深度转换)、shallowEnhancer(仅转换集合本身)、referenceEnhancer(只跟踪引用)与refStructEnhancer(引用 + 结构相等比较),分别对应observableobservable.shallowobservable.refobservable.struct四种注解。observable.ts#L62-L71 展示了各注解与其增强器的绑定关系。

测试用例 "class - annotations" 系统验证了各注解的实际效果:observable.ref字段可观察但内部对象不转换、observable.shallow字段可观察且集合本身可观察但内容不转换、action留在原型上而action.bound/flow.bound以自有属性形式落到实例上。

限制(Limitations)

以下限制在采用注解 API 前必须了解:

  1. make(Auto)Observable只支持已经定义的属性。请确保你的编译器配置正确(参见 使用符合规范的 class properties 转译),或作为变通方案,在使用make(Auto)Observable之前给所有属性赋值。若配置不正确,声明但未初始化的字段(如class X { y; })将不能被正确拾取。
  2. makeObservable只能注解本类定义中声明的属性。如果父类或子类引入了可观察字段,它们需要为那些属性自行调用makeObservable
  3. options参数只能提供一次。传入的options是**粘性(sticky)**的,之后(例如在子类中)无法更改。
  4. 每个字段只能被注解一次override除外)。字段的注解或配置在子类中不能改变。测试 "subclass - cannot re-annotate" 验证了重复注解会抛出Cannot apply错误。
  5. 非普通对象()的所有被注解字段都是non-configurable的。
    可用configure({ safeDescriptors: false })关闭 {🚀☣️}。默认值见 globalstate.ts#L155(safeDescriptors = true)。
  6. 所有非 observable(无状态)字段actionflow)都是non-writable的。
    可用configure({ safeDescriptors: false })关闭 {🚀☣️}。该行为可在 action 注解中writable: safeDescriptors ? false : true的实现中确认。
  7. 只有定义在原型上actioncomputedflowaction.bound才能被子类覆盖(subclassing)。
  8. 默认情况下TypeScript不允许你注解private字段。可以通过显式地把相关私有字段作为泛型参数传入来解决,例如:makeObservable<MyStore, "privateField" | "privateField2">(this, { privateField: observable, privateField2: observable })(参见 测试 "makeObservable supports private fields")。
  9. 调用make(Auto)Observable并提供注解必须是无条件的,这样才能缓存推断结果。
  10. make(Auto)Observable调用之后修改原型是不被支持的。
  11. EcmaScript**私有字段(#field)**不被make(Auto)Observable支持。请改用 auto-accessor + Stage-3 装饰器(@observable accessor #field)语法。否则,使用TypeScript时建议用private修饰符。
  12. 在单个继承链中混用注解与装饰器是不被支持的——例如不能父类用装饰器、子类用注解。
  13. makeObservableextendObservable不能用于其他内建可观察类型(ObservableMapObservableSetObservableArray等)。测试 "Extending builtins is not support #2765" 确认了扩展ObservableMap/ObservableSet会抛出 "Extending builtins is not supported" 错误。
  14. makeObservable(Object.create(prototype))会把prototype上的属性复制到创建的对象并制成observable。这种行为是错误的、出乎意料的,因此已弃用,未来版本很可能改变。不要依赖它。

Options 选项 {🚀}

上述 API 都接受一个可选的options参数,这是一个支持以下选项的对象(类型定义见 CreateObservableOptions):

  • autoBind: true:默认使用action.bound/flow.bound,而不是action/flow。不影响显式注解的成员。测试 "makeObservable supports autoBind" 验证了开启后t.actionBound.call(undefined)仍正确返回实例t
  • deep: false:默认使用observable.ref,而不是observable。不影响显式注解的成员。测试 "makeAutoObservable respects options.deep #2542" 验证了deep: false时嵌套对象保持非可观察。
  • name: <string>:给对象一个调试名,会打印在错误信息和反射 API 中。测试 "makeObservable respects options.name #2614" 验证了getDebugName(instance)返回该名称。

说明:options 是粘性的,只能提供一次

options参数只能为尚未可观察target提供。
一旦可观察对象初始化,就无法更改 options。
options 存储于 target 上,后续对同一 target 的makeObservable/extendObservable调用会遵循它。
你不能在子类中传入不同的 options。

把可观察对象转换回原生 JavaScript 集合

有时需要把可观察数据结构转回原生对应物。例如把可观察对象传给无法追踪可观察对象的 React 组件,或需要一个不再被进一步修改的克隆。

浅转换使用常规 JavaScript 机制即可:

const plainObject = { ...observableObject } const plainArray = observableArray.slice() const plainMap = new Map(observableMap)

要递归地把数据树转为普通对象,可以使用toJS工具函数。其实现 有几个值得注意的细节:它用Map缓存已访问节点以正确处理循环引用;可观察值/计算属性会取.get()后的结果;不会递归进入非可观察值(即使它们内部含有可观察对象);computed 及其他不可枚举属性会被完全忽略。对于类,推荐实现toJSON()方法,因为它会被JSON.stringify自动拾取。

关于类的简短说明

目前为止的示例大多偏向类语法。MobX 在原则上并不强加这种偏好,使用普通对象的 MobX 用户可能同样多。但类有一些轻微优势:API 更易于发现(例如配合 TypeScript);instanceof检查对类型推断非常有用;类实例不会被包装在 Proxy 中,调试器中的体验更好;最后,由于类的形状可预测、方法共享在原型上,它们受益于大量引擎优化。但重的继承模式很容易成为陷阱,所以如果使用类,请保持简单。虽然总体上略微偏好类,但如果普通对象风格更适合你,当然也鼓励你偏离这种风格。

延伸阅读

  • actions:action/flow的完整用法与enforceActions配置;
  • computeds:computed的缓存与失效机制;
  • subclassing:继承场景下override注解的正确用法;
  • api.md:observable.boxObservableArrayObservableMapObservableSettoJS的完整 API 参考;
  • reactions.md:autorunreaction等如何消费可观察状态;
  • 相关测试文件:make-observable.ts、observables.js。

【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询