【问题标题】:Mobx how to cache computed values?Mobx 如何缓存计算值?
【发布时间】:2020-04-09 20:17:17
【问题描述】:

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

这样的实体类
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 帮助器定义了计算,然后不应该将其用作吸气剂?调试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;
  }
}

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

【问题讨论】:

    标签: javascript mobx


    【解决方案1】:

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

    虽然有一点内存泄漏的风险:如果计算对象引用的任何内容仍然存在,则计算对象将不会被清理。但是,如果你只是指类本地的东西,这应该被保存。

    【讨论】:

      【解决方案2】:

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

      只要computed 值没有被reaction 使用,它就不会被记忆,所以它就像一个普通的急切评估函数。如果您要在 autorun 中使用 [getter],此行为将会改变,您将不会看到不必要的计算。

      ...

      MobX 以这种方式工作的原因是,只要 computed 值没有被某些 reaction 使用,它就可以被忽略。 MobX 不会重新计算所有内容,并且该计算不会使任何其他计算保持活动状态。

      但要小心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!
         }
      }
      

      【讨论】:

      • 我将接受并赏金这个答案,尽管听起来像下面的每个 mweststrate 的答案听起来 keepAlive 值将满足这个问题,但它又没有完全记录或清楚解释。有点失望这种模式有如此混乱的界面。
      【解决方案3】:

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

      这个问题没有好办法。

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

      在以下示例中,请注意 cacheComputed() 函数。它将实例和属性缓存为 string 并简单地将autorun 包裹在它周围。我们在类的constructor 中使用它。此外,如果您要处理实例本身,请确保使用 autorun 中的 dispose。为此,我通常在实例上有一个 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

      【讨论】:

      • 感谢我曾考虑编写另一个装饰器来完成 cacheComputed 函数所做的工作
      猜你喜欢
      • 1970-01-01
      • 2019-06-21
      • 2019-03-24
      • 2023-03-15
      • 2014-12-14
      • 2012-05-05
      • 2022-12-06
      • 2019-10-05
      • 1970-01-01
      相关资源
      最近更新 更多