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

推荐订阅源

aimingoo的专栏
aimingoo的专栏
V
V2EX
G
Google Developers Blog
F
Full Disclosure
Martin Fowler
Martin Fowler
宝玉的分享
宝玉的分享
H
Hacker News: Front Page
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
NISL@THU
NISL@THU
G
GRAHAM CLULEY
V
Vulnerabilities – Threatpost
Hacker News - Newest:
Hacker News - Newest: "LLM"
A
About on SuperTechFans
The Cloudflare Blog
C
Cisco Blogs
D
DataBreaches.Net
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
P
Privacy International News Feed
Microsoft Security Blog
Microsoft Security Blog
Help Net Security
Help Net Security
Recorded Future
Recorded Future
PCI Perspectives
PCI Perspectives
S
Schneier on Security
AI
AI
N
News | PayPal Newsroom
雷峰网
雷峰网
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Proofpoint News Feed
The Last Watchdog
The Last Watchdog
L
LINUX DO - 最新话题
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research
Schneier on Security
Schneier on Security
S
Securelist
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
博客园_首页
AWS News Blog
AWS News Blog
TaoSecurity Blog
TaoSecurity Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - 三生石上(FineUI控件)
C
CXSECURITY Database RSS Feed - CXSecurity.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Cloudbric
Cloudbric
C
Cybersecurity and Infrastructure Security Agency CISA
Project Zero
Project Zero
C
Check Point Blog
S
Security Affairs

一颗小树

周更的第 100 篇 改变阅读方式 宝贵的人生建议 小树的工具库 2023 读《重构》有感 仅需 10 分钟,用 GPTs 实现文章总结助手 高质量的需求交付 产品始于问题,而不是解决方案 如何更好地休息 最优解人生 与体重斗,其乐无穷 结构化 prompt = 数字员工? 持续创作的法门 如何降低知识焦虑 7 月思维碎片 N 倍生产力提升:我的 AI 助理 Indie Hacker,互联网打工人的下一个出路? 如何成就伟大事业 夏日碎片 小报编辑的自我修养 一颗小树 #49 投入真实生活 一颗小树 #48 过年杂记 一颗小树 #47 我的人生信念(2023) 一颗小树 #46 回本就卖 一颗小树 #45 规划和落地 小树的 2022 年终总结 一颗小树 #44 构建高质量信源 一颗小树 #43 我的 2022 书单 一颗小树 #42 阳了怎么办 一颗小树 #41 一人企业 一颗小树 #40 非必要别正常啦 一颗小树 #39 阅读和创造 一颗小树 #38 疫情生活碎片 一颗小树 #37 基于多维表格的可转债投资系统 一颗小树 #36 飞盘初体验 一颗小树 #35 雁栖湖半日游 一颗小树 #34 初探多维表格 一颗小树 #33 回不去的名字叫家乡 一颗小树 #32 生活需要留白 一颗小树 #31 Figma 的成功之道 一颗小树 #30 拓宽自己 拥抱善意 一颗小树 #29 有关下雨的记忆 一颗小树 #28 隐入尘烟 一颗小树 #27 找到并坚持自己的热爱 一颗小树 #26 好产品的“差”改版 一颗小树 #25 吹吹南澳岛的海风 一颗小树 #24 代码重构的一点心得 一颗小树 #23 和团队新同学的对话 一颗小树 #22 从《干法》中学到的 一颗小树 #21 阿里四年我的收获和感想 一颗小树 #20 周更半年我学到的事 一颗小树 #19 慢慢来比较快 一颗小树 #18 只规划每天最重要的三件事 一颗小树 #17 裁员潮下的反思 一颗小树 #16 纪录片的力量在于真实 一颗小树 #15 世界很小但很好 一颗小树 #14 让自己成为问题的终结者 一颗小树 #13 别忘记你的热爱 一颗小树 #12 聊聊写作这件事 一颗小树 #11 开始投资前更重要的事 一颗小树 #10 疫情囤货指南 一颗小树 #9 疫情之下 重建内心的秩序 一颗小树 #8 不要倒在黎明前 一颗小树 #7 从小孩子身上学到的事 一颗小树 #6 和自己的产品一起成长 一颗小树 #5 技术对生产力的的变革是不可阻挡的 一颗小树 #4 暴风雨的意义 一颗小树 #3 投资成功,是我们变成一个更好的人之后自然的结果 一颗小树 #2 持续学习来应对这个复杂多变的世界 一颗小树 #1 任天堂和上岭村 一颗小树 #0 这是写给你们的第一封信 游戏改变世界 把自己给开源了 构建自己的不为清单 有爱和被爱 做了一点微小的工作 小树的 2021 年终总结 小树的 2020 年终总结 与其感慨路难行,不如马上出发 阿里前端也切图?不,人工智能帮你做 如何写好简历,拿到大厂 offer 的敲门砖 任时光匆匆流去 | 2019 年终总结 聊聊在阿里远程办公那点事儿 在 D2 做主持人是怎样一种体验? 提高工作效率的几个方法 有限状态机在 CSS 动画中的应用 「译」React 项目结构和组件命名之道 编写小而美函数的艺术 使用纯粹的JS构建 Web Component 翻译计划-状态管理请三思 利用Dawn工程化工具实践MobX数据流管理方案 高性能 MobX 模式(part 3)- 用例教程 高性能 MobX 模式(part 2)- 响应变化 技术文章写法浅谈 翻译计划-用node.js开发一个可交互的命令行应用 无他,唯手熟尔-前端实习面试题篇 工欲善其事必先利其器-前端实习简历篇 手把手带你用原生js实现css属性的set和get 我们来翻翻元素样式的族谱-getComputedStyle 翻译计划-从sass到PostCSS
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 等方法:仅在无法避免时使用,并显式释放内存,避免泄露。