(@)computed

egghead.io 第3课: 计算值

计算值(computed values)是可以根据现有的状态或其它计算值衍生出的值。概念上来说,它们与excel表格中的公式十分相似。不要低估计算值,因为它们有助于使实际可修改的状态尽可能的小。此外计算值还是高度优化过的,所以尽可能的多使用它们。

不要把 computedautorun 搞混。它们都是响应式调用的表达式,但是,如果你想响应式的产生一个可以被其它 observer 使用的,请使用 @computed,如果你不想产生一个新值,而想要达到一个效果,请使用 autorun。举例来说,效果是像打印日志、发起网络请求等这样命令式的副作用。

如果任何影响计算值的值发生变化了,计算值将根据状态自动进行衍生。计算值在大多数情况下可以被 MobX 优化的,因为它们被认为是纯函数。例如,如果前一个计算中使用的数据没有更改,计算属性将不会重新运行。如果某个其它计算属性或 reaction 未使用该计算属性,也不会重新运行。在这种情况下,它将被暂停。

这个自动地暂停是非常方便的。如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。而 autorun 中的值必须要手动清理才行,这点和计算值是有所不同的。如果你创建一个计算属性,但不在 reaction 中的任何地方使用它,它不会缓存值并且有些重新计算看起来似乎是没有必要的。这点有时候会让刚接触 MobX 的人们很困惑。然而,在现实开发场景中,这是迄今为止最好的默认逻辑。如果你需要的话,可以使用 observekeepAlive 来强制保持计算值总是处于唤醒状态。

注意计算属性是不可枚举的,它们也不能在继承链中被覆盖。

@computed

如果已经启用 decorators 的话,可以在任意类属性的 getter 上使用 @computed 装饰器来声明式的创建计算属性。

  1. import {observable, computed} from "mobx";
  2. class OrderLine {
  3. @observable price = 0;
  4. @observable amount = 1;
  5. constructor(price) {
  6. this.price = price;
  7. }
  8. @computed get total() {
  9. return this.price * this.amount;
  10. }
  11. }

computed 调节器

如果你的环境不支持装饰器,请使用 computed(expression) 调节器组合 extendObservable / observable 以引入新的计算属性。

从本质上来说,@computed get propertyName() { } 在构造函数中调用的 extendObservable(this, { propertyName: get func() { } }) 的语法糖。

  1. import {extendObservable, computed} from "mobx";
  2. class OrderLine {
  3. constructor(price) {
  4. extendObservable(this, {
  5. price: price,
  6. amount: 1,
  7. // 有效:
  8. get total() {
  9. return this.price * this.amount
  10. },
  11. // 同样有效:
  12. total: computed(function() {
  13. return this.price * this.amount
  14. })
  15. })
  16. }
  17. }

计算值的 setter

还可以为计算值定义 setter。注意这些 setters 不能用来直接改变计算属性的值,但是它们可以用来作“逆向”衍生。例如:

  1. const box = observable({
  2. length: 2,
  3. get squared() {
  4. return this.length * this.length;
  5. },
  6. set squared(value) {
  7. this.length = Math.sqrt(value);
  8. }
  9. });

同样的

  1. class Foo {
  2. @observable length = 2;
  3. @computed get squared() {
  4. return this.length * this.length;
  5. }
  6. set squared(value) { // 这是一个自动的动作,不需要注解
  7. this.length = Math.sqrt(value);
  8. }
  9. }

注意: 永远在 getter 之后 定义 setter,一些 TypeScript 版本会知道声明了两个具有相同名称的属性。

注意: setter 需要 MobX 2.5.1 或者更高版本

computed(expression) 函数

computed 还可以直接当做函数来调用。就像 observable.box(primitive value) 创建一个独立的 observable。在返回的对象上使用 .get() 来获取计算的当前值,或者使用 .observe(callback) 来观察值的改变。这种形式的 computed 不常使用,但在某些情况下,你需要传递一个“在box中”的计算值时,它可能是有用的。

示例:

  1. import {observable, computed} from "mobx";
  2. var name = observable("John");
  3. var upperCaseName = computed(() =>
  4. name.get().toUpperCase()
  5. );
  6. var disposer = upperCaseName.observe(change => console.log(change.newValue));
  7. name.set("Dave");
  8. // 输出: 'DAVE'

computed 的选项

当使用 computed 作为调节器或者盒子,它接收的第二个选项参数对象,选项参数对象有如下可选参数:

  • name: 字符串, 在 spy 和 MobX 开发者工具中使用的调试名称
  • context: 在提供的表达式中使用的 this
  • setter: 要使用的setter函数。 没有 setter 的话无法为计算值分配新值。 如果传递给 computed 的第二个参数是一个函数,那么就把会这个函数作为 setter
  • compareStructural: 默认值是 false。 当为 true 时,表达式的输出在结果上与先前的值进行比较,然后通知任何观察者相关的更改。 这确保了计算的观察者不用重新评估返回的新结构是否等于原始结构。 这在使用点、矢量或颜色结构时非常有用。通过将 equals 选项指定为 comparer.structural 可以实现同样的行为。
  • equals: 默认值是 comparer.default 。它充当比较前一个值和后一个值的比较函数。如果这个函数认为前一个值和后一个值是相等的,那么观察者就不会重新评估。这在使用结构数据和来自其他库的类型时很有用。例如,一个 computed 的 moment 实例可以使用 (a, b) => a.isSame(b) 。此选项如果指定的话,会覆盖 compareStructural 选项。

@computed.struct 用于比较结构

@computed 装饰器不需要接收参数。如果你想创建一个能进行结构比较的计算属性时,请使用 @computed.struct

@computed.equals 用于自定义比较

如果你想创建一个使用自定义比较的计算属性,请使用 @computed.equals(comparer)

内置比较器

MobX 提供了三个内置 comparer (比较器) ,它们应该能满足绝大部分需求:

  • comparer.identity: 使用恒等 (===) 运算符来判定两个值是否相同。
  • comparer.default: 等同于 comparer.identity,但还认为 NaN 等于 NaN
  • comparer.structural: 执行深层结构比较以确定两个值是否相同。

错误处理

如果计算值在其计算期间抛出异常,则此异常将捕获并在读取其值时重新抛出。强烈建议始终抛出“错误”,以便保留原始堆栈跟踪。 例如:throw new Error(“Uhoh”), 而不是throw "Uhoh"。抛出异常不会中断跟踪,所有计算值可以从异常中恢复。

示例:

  1. const x = observable(3)
  2. const y = observable(1)
  3. const divided = computed(() => {
  4. if (y.get() === 0)
  5. throw new Error("Division by zero")
  6. return x.get() / y.get()
  7. })
  8. divided.get() // 返回 3
  9. y.set(0) // OK
  10. divided.get() // 报错: Division by zero
  11. divided.get() // 报错: Division by zero
  12. y.set(2)
  13. divided.get() // 已恢复; 返回 1.5