尽管 observables 没有改变,但 MobX 在读取时完全重新计算?

MobX full recompute on read though observables don't change?

我有一个看起来像

的 mobx 商店
class EntityStore {
  rootStore
  @observable entityIndex = {}
  constructor(rootStore){
    this.rootStore = rootStore;
  }

  @action addEntities(entityArray){
    entityArray.forEach((entity) => this.addEntity(entity));
  }

  @action addEntity(entityProps){
    if(entityProps.id && this.entityIndex[entityProps.id]){
      throw new Error(`Failed to add ${entityProps.id} an entity with that id already exists`);
    }

    const entity = new Entity({
      ...entityProps,
      assetStore: this.rootStore.assetStore,
      regl,
    });
    this.entityIndex[entity.id] = entity;
  }

  @computed get entityAssetIds(){
    return Object.values(this.entityIndex).map(e => e.assetId);
  }

  @computed get renderPayload(){
    const payloadArray = [];
    for(let entityId in this.entityIndex){
      payloadArray.push(this.entityIndex[entityId].renderPayload)
    }
    return payloadArray;
  }
}

这是一个昂贵的计算值,其子计算值在 requestAnimationFrame 循环中调用,该循环以 60fps 调用 entityStore.renderPayload()。我需要将其缓存。

使用trace我得到了输出 [mobx.trace] 'EntityStore@1.renderPayload' is being read outside a reactive context. Doing a full recompute

这让我感到惊讶,因为我的期望是 Mobx 只会在计算值的依赖可观察量发生变化时才重新计算。

有什么方法可以强制这种非重新计算行为吗?

更新:我没有使用反应。这是普通的 mobx 对象 我在 https://github.com/kevzettler/mobx_bad/ 创建了一个复制示例 看起来像

class ShouldMemoize {
  @observable position = 0
  staticValue = 200;

  @computed get expensiveStaticOperation(){
    console.log('this is expensive and should not recompute');
    return this.staticValue*2;
  }

  @computed get output(){
    return this.position + this.expensiveStaticOperation;
  }

  @action incrementPosition(val){
    this.position+= 1;
  }
}


let M = new ShouldMemoize();
console.log('**********start*********');
setInterval(() =>{
  M.incrementPosition();
  console.log(
    "*tick*",
    M.output
  );
}, 60)

此示例演示了从另一个计算方法 output 引用的计算方法 expensiveStaticOperationoutput 方法被快速调用,日志输出表明 expensiveStaticOperation 然后也被调用并在每个滴答时重新计算。我希望因为 expensiveStaticOperation 的依赖值不会改变,所以它会被记忆而不是重新执行。

这通常是您尝试在不使用 observer(或 reactionautorun)的情况下访问该值时出现的行为。您没有提及您是否使用 React 或其他库,也没有解释您如何访问该值。可能值得在 CodeSandbox 或类似的网站上设置一个最小的示例来重现您所看到的问题。

如果我不得不推测,我会说您正在某个组件中的某处使用您的计算值,该组件未用 observer 包装(在 React 的情况下)。 Here's 一个在没有额外框架的情况下重现问题的示例(删除了未使用的方法):

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

class EntityStore {
  @observable entityIndex = {};

  @computed get renderPayload() {
    const payloadArray = [];
    for (let entityId in this.entityIndex) {
      payloadArray.push(this.entityIndex[entityId].renderPayload);
    }
    return payloadArray;
  }
}
configure({ computedRequiresReaction: true });

const store = new EntityStore();

// console.log('THIS WILL TRIGGER A WARNING:', store.renderPayload)

autorun(() => console.log('THIS WILL NOT TRIGGER A WARNING:', store.renderPayload))

如果取消注释日志,您应该会看到控制台警告。很有可能,您正在 observer 或类似的地方之外的某个地方使用计算值。如果您确定情况并非如此,也许可以在问题中添加更多细节。

Mobx 并不像您在 repo 中用示例命名它那样糟糕,相反,它非常棒,不会重新计算任何代码未观察到的那些值。

要修复您的示例,只需将这一行放在 setInterval

之前
observe(M, 'output', () => {})

现在 MobX 知道输出被观察到,你昂贵的计算只会 运行 一次。直到一些相关的变量发生变化。

只需在开始动画循环之前观察使用过的计算字段,并在结束后进行处理,它会非常有效。