MobX Computed 完全指南:用 computed 派生状态、缓存结果与优化响应式性能
发布时间:2026/9/19 1:35:42 锦皓数字建站

MobX Computed 完全指南用 computed 派生状态、缓存结果与优化响应式性能【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx导读本文围绕 MobX 的computed计算值机制展开讲解如何用最少的状态、以声明式的方式派生信息computed 惰性求值、缓存输出、只在底层 observable 变化时重算并在无人观察时自动挂起。读完本文你将掌握computed的五种使用形态注解、选项、独立函数、getter 装饰器、其挂起/缓存/通知的底层原理、computed.struct与equals等高级选项的取舍以及computed 是否一定要用的工程判断配套示例可直接在浏览器 Console 或 Node 环境运行验证。说明本文为 docs/computeds.md 的深度展开所有源码依据均来自本仓库packages/mobx包。一、computed 是什么响应式状态里的电子表格公式Computed values 用于从其他 observable派生信息。它们惰性求值lazy只有被读取时才计算缓存输出caching底层 observable 未变化时多次读取直接返回缓存只读重算recompute only if needed仅当所依赖的 observable 发生变化时才重新计算完全挂起suspend当没有任何 reaction 观察它时计算被整体暂停。从概念上讲computed 与电子表格中的公式高度相似你只维护原始单元格state其余全部由公式derivation推导。它帮你大幅减少需要存储的状态量并且经过高度优化——能用 computed 的地方尽量用。computed在 MobX 中有五种使用形态见 packages/mobx/src/api/computed.ts 中工厂函数的实际分支形态写法适用场景注解computed与makeObservable配合标注 getter注解 选项computed(options)需要equals、keepAlive等定制独立函数computed(fn, options?)创建盒子化的独立计算值返回.get()/.set()对象getter 装饰器computed启用装饰器语法后标注 getter装饰器 选项computed(options)同上并携带选项其中computed(fn, options?)会直接返回一个ComputedValue实例实现于 packages/mobx/src/core/computedvalue.ts可用.get()读取、.set()写入若配置了 setter。二、基本用法注解 getter makeObservable创建 computed 的标准方式是用computed注解一个 JavaScript getter用makeObservable显式把某个 getter 声明为 computed如果想让类中所有 getter 自动成为 computed可以直接使用makeAutoObservable、observable或extendObservablecomputed getter 会成为**不可枚举non-enumerable**属性。官方示例原文代码可直接运行import { makeObservable, observable, computed, autorun } from mobx class OrderLine { price 0 amount 1 constructor(price) { makeObservable(this, { price: observable, amount: observable, total: computed }) this.price price } get total() { console.log(Computing...) return this.price * this.amount } } const order new OrderLine(0) const stop autorun(() { console.log(Total: order.total) }) // Computing... // Total: 0 console.log(order.total) // (No recomputing!) // 0 order.amount 5 // Computing... // (No autorun) order.price 2 // Computing... // Total: 10 stop() order.price 3 // Neither the computation nor autorun will be recomputed.示例中的autorun来自 Reactions 进阶章节它创建了一个响应式副作用。下面逐行拆解这个输出理解 computed 的缓存点价值autorun首次执行读取order.total→ 触发一次计算Computing...输出Total: 0console.log(order.total)→不再重算直接返回缓存 0order.amount 5→total重算了一次Computing...但由于5 * 0 0结果没变autorun 没有被触发order.price 2→ 重算Computing...输出Total: 10此时才通知 autorunstop()之后修改order.price 3→既不算也不通知因为已经没有任何观察者。关键在于第 3 步amount变化触发了total重算但total检测到输出未被影响仍是 0于是无需更新 autorun。作为对比如果total没有被打上computed注解autorun 将直接依赖price和amount两个原始值上面的操作会触发 effect3 次。三、computed 的依赖图与缓存点原理下面这张依赖图直观展示了上述示例创建出的响应式拓扑图中price、amount是 observable 叶子节点total是 computed 中间节点autorun是最下游的反应。computed 之所以高效是因为它把宽的依赖扇入收敛为窄的通知扇出reaction 只订阅total而不必分别订阅每个底层 observable。从源码看这一机制的实现位于 packages/mobx/src/core/computedvalue.ts 的get()方法其状态机注释概括了完整生命周期同文件 L65-L74首次访问时计算并记住结果之后一直返回缓存任一深层依赖变化时向所有观察者广播POSSIBLY_STALE等待下一步再次被访问时若浅层依赖确实变化则重算若结果变化则向POSSIBLY_STALE的观察者广播STALE否则不通知若在批处理之外且无人观察则重置一切回到第 1 步即挂起。重算时的结果是否变化由equals_比较器判定同文件trackAndCompute()L250-L279const changed wasSuspended || isCaughtException(oldValue) || isCaughtException(newValue) || !this.equals_(oldValue, newValue)这就是第 2 步amount变了但total结果没变、因而不通知 autorun的源码级答案。另外值得注意计算期间 MobX 会通过allowStateChangesStart(false)禁止状态写入L281-L304从机制上保证computed 不应有副作用这一规则。四、computed 的使用规则best practices使用 computed 时官方文档给出三条必须遵守的最佳实践不应有副作用也不应更新其他 observable——computed 是纯派生其内部应只读取、不写入源码层面 MobX 会在计算期间强制关闭状态变更见上文computeValue_的实现违反时在开发模式下会报错避免创建并返回新的 observable——例如不要在 getter 里observable.box(...)否则每次求值都在制造新的响应式节点破坏缓存与通知的稳定性不应依赖非 observable 值——computed 只跟踪其在求值过程中读取的 observable若依赖普通变量变化时无法触发重算缓存会失真。此外还有一个隐含前提computed只能标注在 getter可含 setter属性上。源码 packages/mobx/src/types/computedannotation.ts 的assertComputedDescriptor在开发模式下会直接报错computed can only be used on getter(setter) properties.五、深入理解挂起suspension与 keepAliveTip不被观察的 computed 会被挂起。这是 MobX 新手尤其是从 Reselect 迁移过来最容易困惑的点如果你创建了一个 computed 属性但从未在任何 reaction 中使用它那么它不会被 memoize看起来重算得比预期频繁。例如在上面示例调用stop()之后如果连续两次console.log(order.total)值会被重算两次。官方解释如下MobX 会自动挂起未被积极使用的计算避免对未被访问的 computed 做无谓更新但如果 computed 没有被任何 reaction 使用则每次请求其值都会重新求值——行为就像一个普通属性单独摆弄 computed 时它看起来并不高效但一旦接入observer、autorun等场景它就变得极其高效。演示代码// OrderLine has a computed property total. const line new OrderLine(2.0) // If you access line.total outside of a reaction, it is recomputed every time. setInterval(() { console.log(line.total) }, 60)如果确实需要无人观察也保持缓存有两种绕法给注解传入keepAlive选项创建一个 no-op 反应如autorun(() { someObject.someComputed })后续不再需要时再清理掉。警告以上两种方案都有造成内存泄漏的风险。改变默认行为属于反模式anti-pattern官方明确建议默认不要改动仅在确有必要时使用。另一种更推荐的严格化手段是开启全局配置computedRequiresReaction见 docs/configuration.md它会在computed 在响应式上下文之外被访问时报错从而保证你不会在 MobX 无法缓存它的地方使用它。该配置在源码 packages/mobx/src/api/configure.ts 中被读取并写入globalState.computedRequiresReaction随后由 packages/mobx/src/core/computedvalue.ts 的warnAboutUntrackedRead_()触发警告[mobx] Computed value name is being read outside a reactive context. Doing a full recompute.典型的严格模式配置always级别如下与 docs/configuration.md 中的Linting options一致import { configure } from mobx configure({ enforceActions: always, computedRequiresReaction: true, reactionRequiresObservable: true, observableRequiresReaction: true, disableErrorBoundaries: true })六、为 computed 定义 setter作为推导的逆运算Tipcomputed 也可以有 setter。computed 的 setter不能直接修改计算值本身它没有可写存储但可以用作推导的逆运算inverse of the derivation——即把目标值翻译回对底层 state 的赋值。setter 会被自动标记为 action源码见 packages/mobx/src/core/computedvalue.tsoptions.set被createAction包裹因此修改 state 不会违反enforceActions规则。官方示例class Dimension { length 2 constructor() { makeAutoObservable(this) } get squared() { return this.length * this.length } set squared(value) { this.length Math.sqrt(value) } }这里squared的 getter 派生自length而squared的 setter 把面积反解为边长。注意因为类中所有 getter 会自动成为 computedmakeAutoObservable的行为带 setter 的 getter 会自动成为可写的 computed。七、computed.struct输出做结构化比较Tipcomputed.struct用于结构化比较输出。默认情况下computed 用引用相等reference equality判断新旧输出是否相同。当 computed 每次返回一个新的聚合对象时它永远不可能与上一次输出引用相等观察者会被无谓地通知。computed.struct则先做结构化比较再决定是否通知观察者class Box { width 0 height 0 constructor() { makeObservable(this, { width: observable, height: observable, topRight: computed.struct }) } get topRight() { return { x: this.width, y: this.height } } }源码中computed.struct的实现非常直接packages/mobx/src/api/computed.tsconst computedStructAnnotation createComputedAnnotation(COMPUTED_STRUCT, { equals: compareStructural })即computed.struct本质就是computedequals: compareStructural。不过官方在示例后立刻给出一段泼冷水的说明上面的例子其实不需要computed.struct因为 computed 只在底层值变化时才重算——topRight只会响应width或height的变化而只要其中一个变了topRight的坐标本来就会不同computed.struct永远不会命中缓存纯属浪费。实战结论computed.struct的实际用处比听起来小。只在底层 observable 变化后输出仍可能相同时才使用它——例如先把坐标取整再返回取整后的坐标可能和上一次取整结果相等尽管原始值已经变了get topRight() { return { x: Math.round(this.width), y: Math.round(this.height) } }更通用的如何判定输出是否变化的定制入口是equals选项见下文。八、带参数的推导computed 与参数的四条路径Tipcomputed 值带参数怎么办computed注解只能作用于 getter而 getter 不能接收参数。但派生信息常常需要参数典型场景是 React 多选列表store.isSelected(item.id)。官方在 docs/computeds-with-args.md 中给出了四种方案这里摘要如下派生不一定要是 computed纯函数未加computed也能被 MobX 跟踪——computed 只是缓存点。若派生是纯的有无computed不改变行为只是略微不高效。observer组件会直接订阅函数执行过程中读取到的所有 observable。这是默认策略直到数据证明需要优化闭包捕获参数在 reaction如渲染函数内部临时创建独立 computed——const isSelected computed(() store.isSelected(item.id)).get()。每次渲染创建新 computed 作为缓存点旧的自动被清理组件只在isSelected翻转时重渲染。这是高级优化技巧移动状态把选择状态下沉到Item上作为 observablestore 里的选择集用 computed 表达get selection() { return this.items.filter(item item.isSelected) }使用computedFn来自mobx-utils按参数组合自动 memoize 输出。官方提醒不要过早使用——memoization 需要先想清楚函数会被多少种不同参数调用以便评估内存占用不过它会在结果不再被 reaction 观察时自动清理条目正常情况不会泄漏。关于方案 2 中computed(fn)独立形态见下文第九节。九、独立计算值computed(expression)Tip用computed(expression)创建独立计算值。computed还可以直接作为函数调用类似observable.box会创建一个独立的计算值对象。用返回对象上的.get()获取当前计算结果.set()触发其 setter若有。这种形式并不常用但在需要把盒子化的 computed 值传来传去时非常有用例如 docs/computeds-with-args.md 讨论的在 reaction 中临时创建缓存点场景。源码层面computed(fn, options?)的完整签名与实现packages/mobx/src/api/computed.ts做了三件事校验第一个参数必须是函数开发模式下否则调用die报错解析可选的options必须是普通对象opts.get arg1并把函数名作为默认name然后new ComputedValue(opts)。注意以函数作为第二个参数传 setter 的旧式写法已不再支持需改用{ set: fn }选项违反时开发模式下会报错同文件 L54-L58。返回的IComputedValueT接口定义于 packages/mobx/src/core/computedvalue.ts{ get(): T; set(value: T): void }。十、computed 的 Options 详解computed默认开箱即用但可以通过options参数定制行为。完整的IComputedValueOptionsT定义见 packages/mobx/src/core/computedvalue.tsexport interface IComputedValueOptionsT { get?: () T set?: (value: T) void name?: string equals?: IEqualsComparerT context?: any requiresReaction?: boolean keepAlive?: boolean }下面逐一展开文档重点讲解的四个选项。name字符串用作Spy 事件监听器见 docs/analyzing-reactivity.md和MobX 开发者工具中的调试名称。若不提供开发模式下会生成ComputedValueid之类的自动名packages/mobx/src/core/computedvalue.ts。equals默认值compareDefault。它是一个比较函数用于比较旧值与新值若判定相等则观察者不会被重新求值见上文trackAndCompute()中!this.equals_(oldValue, newValue)的判断。典型用途处理结构化数据或其他库的类型。例如一个 moment 实例的 computed可以用(a, b) a.isSame(b)作为比较器——这样即使底层触发重算只要时间语义相同观察者就免于更新。MobX 内置四个比较函数源码见 packages/mobx/src/utils/comparer.ts并从 packages/mobx/src/mobx.ts 统一导出覆盖equals选项的大多数需求比较器语义源码实现compareIdentity引用同一性a bcompareDefaultObject.is判等Object.iscompareStructural深度结构化比较deepEqual(a, b)compareShallow浅层结构化比较deepEqual(a, b, 1)这四个函数可以从mobx直接导入同样可用于reaction的equals选项packages/mobx/src/api/autorun.ts 中 reaction 默认也用compareDefault。关于computed.equals的行为仓库测试覆盖了三种标注体系Babel 装饰器测试、TypeScript 装饰器测试以及 2022.3 装饰器测试验证了结果引用不同但 equals 判定相等时不通知观察者这一核心语义。requiresReaction强烈建议对非常昂贵的 computed 设为true。当你在响应式上下文之外读取它此时可能无法缓存computed 会抛出异常而不是做一次昂贵的重算。这比静默重算更能暴露错误使用位置。它与全局配置computedRequiresReaction的关系是单个选项优先未设置时回落到全局配置见 packages/mobx/src/core/computedvalue.ts 的warnAboutUntrackedRead_。keepAlivetrue时即使没有观察者computed 也不会被挂起见第五节。代价是可能造成内存泄漏——与 reactions 文档 中讨论的泄漏风险类似computed 会持续持有其依赖树与结果若对象本身已无观察者该对象与派生链上的所有节点都无法被垃圾回收。源码中suspend_()packages/mobx/src/core/computedvalue.ts会显式跳过keepAlive_的计算值get()中批外且无观察者的分支同样以!this.keepAlive_为前提L201-L212。十一、常见误区与调试建议为什么我的 computed 总是重算—— 先检查它是否被任何 reaction 观察未被观察时每次读取都会重算第五节。这是正常行为不是 bugcomputed 里能改状态吗—— 不能。计算期间 MobX 会关闭 state 变更allowStateChangesStart(false)需要写状态请改到 action 里用 computed 还是普通函数—— 纯派生函数默认就是对的computed 只是缓存点当重算开销显著或下游观察者众多时才需要它第八节方案 1 的结论什么时候该用 computed.struct—— 仅当底层值变化后输出仍可能相等如取整场景时否则是纯浪费第七节如何排查 computed 的求值/通知—— 开启name选项配合spy事件监听或在 MobX 开发者工具 介绍的分析手段。小结computed是 MobX 最小状态、最大化派生哲学的核心构件它惰性求值、缓存输出、无人观察时挂起把宽依赖收敛为窄通知。正确使用姿势总结为用makeObservable/makeAutoObservable把纯 getter 标记为computed遵守三条规则无副作用、不产新 observable、不依赖非 observable默认不开keepAlive用computedRequiresReaction兜底暴露误用输出为聚合对象且可能语义相等时考虑equals: compareStructural或computed.struct带参数的推导优先用普通函数或闭包 computedcomputedFn只在评估过参数组合规模后使用。相关延伸阅读docs/reactions.mdautorun/reaction、docs/computeds-with-args.md带参派生四方案、docs/configuration.mdcomputedRequiresReaction等 lint 配置、docs/enabling-decorators.mdcomputed装饰器启用方式。【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。