
MobX 创建可观察状态makeObservable、makeAutoObservable 与注解系统深度解析【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx本篇聚焦 MobX 中「把属性、对象、数组、Map 和 Set 变为可观察状态」这一核心能力系统讲解makeObservable、makeAutoObservable、observable三种 API 的用法、推断规则、可用注解全集与已知限制并结合 makeObservable 源码、自动注解推断实现 与 测试用例 印证底层行为。读完本文你可以直接在项目中新建可观察 Store、选择正确的注解组合并理解每条限制背后的源码成因。核心概念状态、action 与 computed 三种注解在 MobX 中属性、整个对象、数组、Map 和 Set 都可以被制成可观察状态。让对象变得可观察的基本手段就是使用makeObservable为每个属性指定一个注解annotation。三种最核心的注解是observable定义一个可追踪的、用于存储状态的字段action把方法标记为会修改状态的 actioncomputed把 getter 标记为从状态派生新事实、并缓存其输出的计算属性。理解这三种注解的职责划分是掌握 MobX 可观察状态体系的基础observable存状态action改状态computed派生状态。makeObservable为已有属性指定注解用法签名makeObservable(target, annotations, options?)该函数用于让已存在的对象属性变得可观察。任何 JavaScript 对象包括类实例都可以作为target传入。典型用法是在类的构造函数中调用makeObservable第一个参数为this。annotations参数是一个把注解映射到各个成员的字典只有被注解的成员会受到影响。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即无状态字段action、flow都是 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未按预期工作请显式指定flowoverrides中标记为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 的实现 可以看到两个关键行为推断结果缓存首次调用时它会把目标实例及其原型的所有 key 收集进一个Set并以隐藏属性keysSymbolSymbol(mobx-keys)缓存到原型上makeObservable.ts#L69-L77。后续实例化无需再遍历原型这正是文档限制中「make(Auto)Observable必须无条件调用」的原因——无条件调用才能安全地复用缓存的推断结果子类检查开发模式下若目标不是普通对象且其原型也不是普通对象直接抛出makeAutoObservable can only be used for classes that dont have a superclassmakeObservable.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.isArray走observable.arrayES6Map/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)Observable与observable的主要区别在于前者修改你传入的原始对象而observable会创建一个克隆并使其可观察。observable会创建一个 Proxy 对象以便在把对象用作动态查找表时能拦截未来的属性添加。如果你想变成可观察的对象具有常规结构、所有成员都预先可知makeObservable往往是更清晰的 API因为它保留了原始对象标识。因此在工厂函数中推荐使用make(Auto)Observable。可用注解全览注解说明observableobservable.deep定义一个可追踪的、存储状态的字段。若可能赋给observable的任何值都会根据其类型自动转换为深observable、autoAction或flow。只有普通对象、数组、Map、Set、函数、生成器函数可被转换。类实例等保持不变。observable.ref与observable类似但只追踪重新赋值。被赋的值完全被忽略不会被自动转换为observable/autoAction/flow。例如当你打算在可观察字段中存储不可变数据时使用它。observable.shallow与observable.ref类似但面向集合。任何被赋的集合都会被制成可观察但集合自身的内容不会变成可观察。observable.struct与observable类似但如果赋的值与当前值结构相等则忽略该赋值。action把方法标记为会修改状态的 action。更多细节参见 actions。不可写。action.bound与action类似但会把 action 绑定到实例从而this始终被设置。不可写。computed可用于 getter将其声明为可缓存的派生值。更多细节参见 computeds。computed.struct与computed类似但如果重算后的结果与上次结果结构相等则不通知观察者。true推断最佳注解。更多细节参见 makeAutoObservable。false明确不对该属性进行注解。flow创建一个flow来管理异步流程。更多细节参见 flow。注意 TypeScript 中推断的返回类型可能不准确。不可写。flow.bound与flow类似但会把 flow 绑定到实例从而this始终被设置。不可写。override适用于子类覆盖父类的action、flow、computed、action.bound。autoAction不应显式使用它是makeAutoObservable在底层用来标记「可以既作为 action 又作为派生」的方法的注解运行时会按调用上下文判定该函数本次是派生还是 action。这些注解的底层差异体现在增强器enhancer上modifiers.ts 定义了deepEnhancer默认深度转换、shallowEnhancer仅转换集合本身、referenceEnhancer只跟踪引用与refStructEnhancer引用 结构相等比较分别对应observable、observable.shallow、observable.ref、observable.struct四种注解。observable.ts#L62-L71 展示了各注解与其增强器的绑定关系。测试用例 class - annotations 系统验证了各注解的实际效果observable.ref字段可观察但内部对象不转换、observable.shallow字段可观察且集合本身可观察但内容不转换、action留在原型上而action.bound/flow.bound以自有属性形式落到实例上。限制Limitations以下限制在采用注解 API 前必须了解make(Auto)Observable只支持已经定义的属性。请确保你的编译器配置正确参见 使用符合规范的 class properties 转译或作为变通方案在使用make(Auto)Observable之前给所有属性赋值。若配置不正确声明但未初始化的字段如class X { y; }将不能被正确拾取。makeObservable只能注解本类定义中声明的属性。如果父类或子类引入了可观察字段它们需要为那些属性自行调用makeObservable。options参数只能提供一次。传入的options是**粘性sticky**的之后例如在子类中无法更改。每个字段只能被注解一次override除外。字段的注解或配置在子类中不能改变。测试 subclass - cannot re-annotate 验证了重复注解会抛出Cannot apply错误。非普通对象类的所有被注解字段都是non-configurable的。可用configure({ safeDescriptors: false })关闭 {☣️}。默认值见 globalstate.ts#L155safeDescriptors true。所有非 observable无状态字段action、flow都是non-writable的。可用configure({ safeDescriptors: false })关闭 {☣️}。该行为可在 action 注解中writable: safeDescriptors ? false : true的实现中确认。只有定义在原型上的action、computed、flow、action.bound才能被子类覆盖subclassing。默认情况下TypeScript不允许你注解private字段。可以通过显式地把相关私有字段作为泛型参数传入来解决例如makeObservableMyStore, privateField | privateField2(this, { privateField: observable, privateField2: observable })参见 测试 makeObservable supports private fields。调用make(Auto)Observable并提供注解必须是无条件的这样才能缓存推断结果。make(Auto)Observable调用之后修改原型是不被支持的。EcmaScript**私有字段#field**不被make(Auto)Observable支持。请改用 auto-accessor Stage-3 装饰器observable accessor #field语法。否则使用TypeScript时建议用private修饰符。在单个继承链中混用注解与装饰器是不被支持的——例如不能父类用装饰器、子类用注解。makeObservable、extendObservable不能用于其他内建可观察类型ObservableMap、ObservableSet、ObservableArray等。测试 Extending builtins is not support #2765 确认了扩展ObservableMap/ObservableSet会抛出 Extending builtins is not supported 错误。makeObservable(Object.create(prototype))会把prototype上的属性复制到创建的对象并制成observable。这种行为是错误的、出乎意料的因此已弃用未来版本很可能改变。不要依赖它。Options 选项 {}上述 API 都接受一个可选的options参数这是一个支持以下选项的对象类型定义见 CreateObservableOptionsautoBind: 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 更易于发现例如配合 TypeScriptinstanceof检查对类型推断非常有用类实例不会被包装在 Proxy 中调试器中的体验更好最后由于类的形状可预测、方法共享在原型上它们受益于大量引擎优化。但重的继承模式很容易成为陷阱所以如果使用类请保持简单。虽然总体上略微偏好类但如果普通对象风格更适合你当然也鼓励你偏离这种风格。延伸阅读actionsaction/flow的完整用法与enforceActions配置computedscomputed的缓存与失效机制subclassing继承场景下override注解的正确用法api.mdobservable.box、ObservableArray、ObservableMap、ObservableSet、toJS的完整 API 参考reactions.mdautorun、reaction等如何消费可观察状态相关测试文件make-observable.ts、observables.js。【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考