惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

一颗小树

小树的 2025 年终总结 AI 下半场 如何快速融入新团队 习惯养成的一点新实践 阿里七年,小树毕业了 小树学装修 - Mesh 组网 为什么忙起来就没有表达欲? 昆明风光好 和 deepseek 创作悬疑短篇 基于大模型搭建内容输出工作流 小树的 2024 年终总结 因地制宜 底层逻辑 不要轻易给自己贴标签 在天津随机漫步 如何通过售卖 Notion 模板获得收入 多维表格的边界 平淡日子里的闪光碎片 驾照加载中 河山大好出去走走 “我们”共同的烦恼 长周期的反馈指标 白河溯溪里收获的时代性 从竞争者到合作者 和客户面对面交流 奔波中的六月 内蒙古赤峰草原之行 我为什么不记账了 周末随想 从客户视角出发 我的职业价值观 扔掉心里的锚 骑行小记 用多维表格实现高质量需求交付 黄山:穿越云雾的山水诗篇 允许自己放空 春日老友记 Cubox 导出至 Obsidian 的工作流优化 让时间慢下来 Sam Altman 对提高个人生产力的建议 精读《GPT4 Technical Report》 AI 带我读论文 让世界更好一点点 改变学习方法 小树的 2024 年计划 小树的 2023 年终总结 小树的 2023 书单 周更的第 100 篇 改变阅读方式 宝贵的人生建议 小树的工具库 2023 读《重构》有感 仅需 10 分钟,用 GPTs 实现文章总结助手 高质量的需求交付 产品始于问题,而不是解决方案 如何更好地休息 最优解人生 租房和生活选择权 搬家整理小记 更适合我的时间管理方式:时间盒 做了几个月大模型产品,我学到了什么 与体重斗,其乐无穷 结构化 prompt = 数字员工? 持续创作的法门 如何降低知识焦虑 7 月思维碎片 N 倍生产力提升:我的 AI 助理 Indie Hacker,互联网打工人的下一个出路? 如何成就伟大事业 夏日碎片 小报编辑的自我修养 提升信噪比:过滤有价值信息的方法 干一行爱一行 提高生活的满足感 做好时间管理的几个建议 差旅杂记 如何快速适应自己不擅长的工作 Make Things Happen 消费的科学与艺术 推荐几本最近读的书 表达的前提是经历 Gradually then suddenly 投资没有最好,只有最适合 投资,是为了更好地生活 如何对待事务性任务 如何打造自己的核心竞争力 把手弄脏:细节藏在过程中 述职之后:见他人和见自己 给 flomo MEMO 做一次断舍离 个人知识管理的困境与改进 Astro 搭建个人博客 一颗小树 #49 投入真实生活 一颗小树 #48 过年杂记 一颗小树 #47 我的人生信念(2023) 一颗小树 #46 回本就卖 一颗小树 #45 规划和落地 小树的 2022 年终总结 一颗小树 #44 构建高质量信源 一颗小树 #43 我的 2022 书单 一颗小树 #42 阳了怎么办
MobX 核心机制探究
2025-09-07 · via 一颗小树

背景

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

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

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

class Person {
  name = "John";

  constructor(name) {
    this.name = name;

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

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

响应式系统的砖和瓦

核心概念

mobx-2

MobX 内部有几个核心角色:

  • Atom:最基础的响应式单元。本身不存储值,只负责 订阅关系。
    • 读取值时触发 reportObserved
    • 修改值时触发 reportChanged,广播给订阅者
  • Observable State:Atom 的子类,包装了实际的值,如 Primitive Values、Array、Object、Set 和 Map 等,不同数据结构会对应不同的实现。

接着,我们来看一看常用的 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

  • ObservableValue 可以看作一个 Box,它可以把任意类型的「值」包装为可观察的值。
  • ObservableObject 和 ObjectArray 更多扮演的是对「结构」的观察,核心的思想就是代理 + 拦截。

Derivation

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

  • ComputedValue :即 computed ,是纯函数式的派生。
  • Reaction:即 autorun , reaction , when 以及 mobx-react-lite 中的 observer 方法。它们用来处理响应状态变化的副作用,例如状态变更同时向 LocalStorage 同步等。

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 来类比:

  • 惰性求值:只有真正访问 C1 时,才会触发计算
  • 依赖追踪:明确知道 A1 和 B1 是 C1 结果的依赖
  • 结果缓存:只要依赖没有发生变化,就不会执行计算,快速获取结果
  • 自动更新:A1 或 B1 的值发生变化时会标脏,再次访问 C1 时触发计算,结果不变是不会通知订阅方

总的来说:

  • autorun 是响应式系统的“终点” ,它消费数据,并将其转化为外部世界的副作用。
  • computed 是响应式系统的“中间件” ,它消费数据,然后生产出新的、可被缓存的、可被其他消费者使用的新数据。

内存都被谁用了?

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

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 是实际占用的内存大小。

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

  • {Host, count, time} 是新创建的 Plain Object
  • {annotationType*, options*, make*, extend*, decorate20223} 是 Annotation 的实例
  • (concatenated string) 是在 dev 模式下给每个 ObservableValue 生成的 name

真正的重点在:

  • ObservableObjectAdministration:每一个 ObservableObject 都需要一个 adm 来管理,负责“结构”
  • <symbol mobx administration>:ObservableObjectAdministration 的“钥匙”,用来快速访问到对应的 adm
  • ObservableValue:来源于每一个属性的 deep Observable,负责“值”
  • Atom:响应式的最小单元,每一个 ObservableValue 都需要
  • Map:ObservableObject 中 _values 的容器,内容是 ObservableValue 的引用
  • Set:ObservableValue 中的 observers_ 订阅者列表(Derivation ),在这个 case 中,是经过 observer 包装的 render 函数,每一个 ObservableValue 都会订阅一次

mobx-7 mobx-8

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

  • 一个 Derivation 对象的固定开销。
  • 一个与 被访问元素数量 成正比的 observing_ 数组的开销。
  • 一个与 被访问元素数量 成正比的 observers_ 反向链接开销。

如果 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);
  }
}

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

  • 复杂结构 → observable.ref
  • 派生数据 → computed
  • 副作用 → autorun(必须手动释放)

总结

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

  • 保持单一数据源:通过依赖注入来实现不同模块之间的引用,避免冗余状态。
  • 复杂数据结构 → 优先使用 observable.ref
    • 若确实需要追踪其中的属性,应为其单独创建 Model,只让必要的属性参与依赖收集。
  • 派生状态 → 优先使用 computed:确保结果可缓存、可复用。
  • 副作用 → 谨慎使用 autorun 等方法:仅在无法避免时使用,并显式释放内存,避免泄露。