Mobx 如何缓存计算值?

Mobx how to cache computed values?

我正在使用 Mobx 构建 webgl 游戏引擎。我没有将它与反应一起使用。我用它来增强实体组件系统。我有实体 class 喜欢

import {observable, observe, computed, autorun} from 'mobx';

class Entity {
  @observable position = [0,0,0]
  @observable rotation = [0,0,0]

  @computed get modelMat(){
    return position * rotation;
  }
}

我像这样使用这个实体:

var ent = new Entity();
entity.position = [0,10,0];
if(entity.modelMat == 6){
  // do something
}

我的理解是直接阅读 modelMat 不是最佳做法。它会导致重新计算计算。它没有被缓存。这对我的游戏引擎是有害的,因为我可能会以 60fps 的速度访问这些计算值。

这对我来说似乎不直观,因为您使用 get 帮助程序定义计算,然后不应该将其用作 getter?调试 computedRequiresReaction 设置可用于防止这种直接计算读取模式。

configure({
  computedRequiresReaction: true
});

那么我的问题是如何缓存或记忆这些将被频繁访问的计算值?为了避免这种情况,我开始使用一种使用自动运行的模式,以便在计算更改时更新局部变量。看起来像:

class Entity {
  @observable position = [0,0,0]
  @observable rotation = [0,0,0]

  modelMat = []

  constructor(){
    autorun(() => {
      this.modelMat = this.computedModelMat()
    })
  }

  @computed get computedModelMat(){
    return position * rotation;
  }
}

这为 class 启用了一个接口,这样 ent.modelMat 仍然可以快速访问,但不会每次都重新计算。对此有更好的建议模式吗?为每个计算都有一个自动运行似乎是多余的。我的一些 classes 最终有许多自动运行处理程序来缓存这些值。

是的,您实际上使用的是推荐的方法:https://github.com/mobxjs/mobx/issues/356

as long as a computed value is not used by a reaction, it is not memoized and so it just like a normal eager evaluating function. If you would would use the [getter] in an autorun this behavior will change and you won't see unnecessary computations.

...

the reason MobX works this way is that as long as a computed value is not in use by some reaction, it can simply be ignored. MobX doesn't recompute it all, and the computation doesn't keep any other computation alive.

但要提防memory leaks。问题中的代码没有泄漏,但我不确定你的所有代码:

const VAT = observable(1.2)
class OrderLine {
   @observable price = 10
   @observable amount = 1
   constructor() {
       // this autorun will be GC-ed together with the current orderline instance
       this.handler = autorun(() => {
           doSomethingWith(this.price * this.amount)
       })
       // this autorun won't be GC-ed together with the current orderline instance
       // since VAT keeps a reference to notify this autorun,
       // which in turn keeps 'this' in scope
       this.handler = autorun(() => {
           doSomethingWith(this.price * this.amount * VAT.get())
       })
       // So, to avoid subtle memory issues, always call..
       this.handler()
       // When the reaction is no longer needed!
   }
}

基本上发生的事情是你正在走出 mobx world 而 mobx 关心它之外发生的事情。在 mobx 系统中,没有任何东西 观察 计算值,因此没有理由将其缓存(在内存中)。

这个问题没有很好的解决办法。

我能为您提供的最好的东西是在尝试按照您想要/需要的方式编写代码时提供更好的开发人员体验。

在下面的示例中,请注意 cacheComputed() 函数。它需要将实例和 属性 缓存为 string 并简单地将 autorun 包裹在 it.We 周围并在 constructor 中使用它class。此外,如果您要处理实例本身,请确保 dispose of the autorun。为此,我通常在实例上有一个 dispose() 方法来处理其中的所有反应。

当你完成所有反应时,你应该总是停止它们。

import { computed, autorun, observable, decorate } from "mobx";

function cacheComputed(instance, prop) {
  return autorun(reaction => {
    return instance[prop];
    //todo - maybe throw if 'prop' does not exist
  });
}

class A {
  constructor() {
    this.firstName = "Bob";
    this.lastName = "Marley";

    this.disposeFullName = cacheComputed(this, "fullName");
  }

  get fullName() {
    console.log("computed");
    return `${this.firstName} ${this.lastName}`;
  }

  dispose() {
    this.disposeFullName();
  }
}

decorate(A, {
  firstName: observable,
  lastName: observable,
  fullName: computed
});

const a = new A();

console.log(a.fullName); //cached
console.log(a.fullName); //cached
console.log(a.fullName); //cached

//--- force recompute
console.log("---- recalculate computed");

a.lastName = "Dylan";
console.log(a.fullName); //recomputed
console.log(a.fullName); //cached
a.dispose(); // after this fullName will be recomputed always

CodeSandbox

上查看

注意 computed 支持 keepAlive 选项,这将强制 mobx 缓存值,即使没有观察者。而且它实际上比使用autorun观察更有效,因为有一些内部优化应用于此标志。

虽然有一点内存泄漏的风险:如果计算引用的任何内容仍然存在,则不会清除计算。但是,如果您仅指 class 本地事物,则应保存。