MobX 核心机制探究

发表于:September 7, 2025 at 09:46 AM

背景

在一些特殊业务场景中,前端页面可能需要承载和展示大规模数据(百万级数据),常见的问题包括:页面渲染卡顿、内存快速膨胀,以及潜在的内存泄露风险。

许多项目使用 MobX 作为状态管理方案,但如果对其机制理解不足,在处理大数据时就容易踩坑。本文将尝试分析其响应式系统在大数据场景下的表现,并给出相应的优化实践。

当我们谈 makeObservable 时我们谈些什么

class Person {
  name = "John";

  constructor(name) {
    this.name = name;

    makeObservable(this, {
      name: observable,
    });
  }
}

makeObservable 的作用,就是将普通对象的属性包装为响应式的 observable 属性,让它们能参与 MobX 的依赖收集和更新机制。

响应式系统的砖和瓦

核心概念

mobx-2

MobX 内部有几个核心角色:

接着,我们来看一看常用的 Observable State 是如何实现的。

export class ObservableValue<T> extends Atom {
  observers_: Set<IDerivation>
  value: T
  public enhancer: IEnhancer<T>
  public set(newValue: T) {
    const oldValue = this.value_
    this.value_ = newValue
    this.reportChanged()
  }
  public get(): T {
    this.reportObserved()
    return this.dehanceValue(this.value_)
  }
}

export class ObservableObjectAdministration {
  keysAtom_: IAtom
  public values_ = new Map<PropertyKey, ObservableValue<any> | ComputedValue<any>>(),
  private pendingKeys_: undefined | Map<PropertyKey, ObservableValue<boolean>>

  keys_(): PropertyKey[] {
    this.keysAtom_.reportObserved()
    return Object.keys(this.target_)
  }

  // 代理 has/delete/ownKeys 等方法
}

mobx-3

export class ObservableArrayAdministration {
  atom_: IAtom;
  readonly values_: any[] = [];

  set_(index: number, newValue: any) {
    // ...
    this.atom_.reportChanged();
    // ...
  }
  // ...
  // 代理 length/map/push/splice 等方法
}

function mapLikeFunc(funcName) {
  return function (callback, thisArg) {
    const adm: ObservableArrayAdministration = this[$mobx];
    adm.atom_.reportObserved();
    const dehancedValues = adm.dehanceValues_(adm.values_);
    return dehancedValues[funcName]((element, index) => {
      return callback.call(thisArg, element, index, this);
    });
  };
}

mobx-4

Derivation

Derivation 可以理解为:任何可以从其他可观察状态(Observable State)中派生出来的值。它分为两种:

Derivation 是订阅者,其中的 observing_ 是发布者,即这个派生状态依赖哪些数据源。

给 Derivation 举个 🌰

const disposer = autorun(() => {
  if (user.showProfile) {
    // 依赖: showProfile, name
    console.log(`Name: ${user.name}`);
  } else {
    // 依赖: showProfile, nickname
    console.log(`Nickname: ${user.nickname}`);
  }
});

mobx-5

通过这种双向关联的方式,MobX 就可以实现精确、高效和无内存泄露的响应式系统。

computed 和 autorun 实现异同

computed 和 autorun 是最常用的两个方法,在依赖收集和变更触发阶段,两者的实现基本一致。

autorun 的主要目标是在状态变化时执行副作用,而 computed 的主要目标是基于已有状态派生出新状态,基于这个前提 computed 做了大量优化。

我们可以用电子表格的公式 C1 = A1 + B1 来类比:

总的来说:

内存都被谁用了?

我们来做个实验,看看一个真实的案例中,内存到底被谁占用了。

import React from "react";
import { makeObservable, observable, action } from "mobx";
import { observer } from "mobx-react-lite";

class AppStore {
  plainJsArray = [];
  mobxObservableArray = [];

  constructor() {
    makeObservable(this, {
      mobxObservableArray: observable,
      createMobxArray: action,
    });
  }

  generateLogData = (count) => {
    const data = [];
    for (let i = 0; i < count; i++) {
      data.push({
        Host: "www.google.com",
        count: "279",
        time: "2025-07-23 15:35:00.000",
      });
    }
    return data;
  };

  createPlainArray = () => {
    console.log("Creating plain JS array...");
    this.plainJsArray = this.generateLogData(100000);
    console.log("Plain JS array created.", this.plainJsArray);
  };

  createMobxArray = () => {
    console.log("Creating MobX observable array...");
    const data = this.generateLogData(100000);
    this.mobxObservableArray = observable(data);
    console.log("MobX observable array created.", this.mobxObservableArray);
  };
}

const store = new AppStore();

const App = observer(() => {
  return (
    <div>
      <h1>MobX Memory Experiment</h1>
      <button onClick={store.createPlainArray}>Create Plain JS Array</button>
      <button onClick={store.createMobxArray}>
        Create MobX Observable Array
      </button>
    </div>
  );
});

export default App;

mobx-6

我们先创建 Plain Object,发现内存增加了约 3MB。

我们再创建相同内容的 Observable Object,内存增加了近 100MB。

这里先解释一个概念:Alloc. Size 是 Allocation Size 的缩写,表示的是:在快照 1 和快照 2 之间,新创建的该类型(Constructor)所有对象的 Shallow Size (自身大小) 的总和。Shallow Size 是实际占用的内存大小。

我们先把不太重要的内容厘清:

真正的重点在:

mobx-7 mobx-8

总的来说, Derivation 观察一个大数组的内存开销可以概括为:

如果 10 万个对象 + 每个属性都有响应式双向关联开销,内存自然暴涨。

在这种情况下,如果 Derivation 内存没有被正确释放,就非常容易导致内存泄露。

因此,对于 Array<Object> 结构的数据,一定要谨慎。使用 makeAutoObservable 或者是 autorun 较多时,就非常容易中招。

讲到这里,还是有点抽象,那么我们来看一个真实的 case:

class DemoStore {
  rawData = [];
  dataAfterTransform = [];

  constructor() {
    makeAutoObservable(this);

    autorun(() => {
      const rawDataSlice = this.rawData.map(e => {
        return {
          ...e,
          name: e.name,
        };
      });

      const output = this.transformData(rawDataSlice);

      this.dataAfterTransform = output;
      this.onDataAfterTransformChange(output);
    });
  }
}

让我们假设 rawData 会有 10W 条数据,并且结构会比示例更复杂。

以上的实现有哪些问题?

  1. 数据派生和副作用都在 autorun 中实现
  2. map 操作实际读取了数组项的所有属性,会极大的增加内存开销
  3. autorun 不会缓存,任意数组项的任意属性变动都会导致 autorun 重新触发
  4. autorun 没有处理 disposer,会导致内存泄露
  5. dataAfterTransform 是外界实际消费的数据,可能会内存开销翻倍

最佳实践

class DemoStore {
  rawData = [];

  constructor() {
    makeObservable(this, {
      rawData: observable.ref,
    });

    // 如果一定要用...
    autorun(() => {
      if (this.dataAfterTransform.length > 0) {
        // ...
      }
    });
  }

  @computed
  get dataAfterTransform() {
    const rawDataSlice = this.rawData.map(e => {
      return {
        ...e,
        name: e.name,
      };
    });
    return this.transformData(rawDataFrame);
  }
}

核心思路:最小化数据访问,避免无谓的响应式依赖关系。

总结

为了降低使用 MobX 的复杂度,可以遵循以下基本原则,这些实践足以覆盖大多数场景: