Fork me on GitHub

MobX

十分钟入门 MobX & React


MobX 是一种简单、可扩展并且身经百战的状态管理解决方案。本教程会在十分钟之内教会你 MobX 中所有重要的概念。MobX 是一个独立的库,但大多数人都把它和 React 放在一起使用,本教程就着重介绍这个组合。

核心理念

状态是每个应用的核心。如果你想打造出漏洞百出又难以管理的应用程序,那么搞出不一致的状态或与徘徊着的局部变量不同步的状态就是最快的方法。因此,许多状态管理解决方案会——比如通过使状态不可变——试图限制你修改状态的方式。但这样就会引出新的问题:数据需要被归一化、引用的完整性无法得到保证,而且万一你喜欢像类这样功能强大的概念,那你几乎没办法再用到它们。

MobX 通过解决根本问题让状态管理又一次变得简单起来:它让不一致的状态不再可能出现。要做到这一点,策略很简单: 确保从应用状态中派生出的一切都将被自动派生出来

从概念上讲, MobX 会把你的应用程序视为一份电子表格。

  1. 首先是 应用状态。就是那些呈图状结构分布、组成你应用状态模型的对象、数组、原始值和引用。这些值是你应用中的“单元格”。
  2. 其次是 derivations。也就是任何可以从你的应用状态中被自动计算出来的值。这些 derivations ——或者计算值——可以是简单的值,比如未完成待办的个数;也可以是复杂的值,比如你待办清单的 HTML 视觉表示。
  3. Reactions 与 derivations 非常相似。但主要的区别在于这些函数不会返回值,而会自动执行一些任务。通常这些任务与 I/O 有关。它们会确保 DOM 的更新或者在对的时间自动发出网络请求。
  4. 最后是 actions。actions 就是所有会修改状态的代码。MobX 会让所有由于你的 actions 引发的应用状态的改变都被全部 derivations 和 reactions 同步而顺畅地自动处理掉。

一个简单的待办 store……

理论已经够多了,与其仔细阅读上面的那些字,看看实际的操作可能会带来更好的理解。为了原创性,我们就从一个简单的待办 store 开始。请注意,以下所有代码块都是可以编辑的,也可以用 运行代码 按钮运行。下面是一个非常简单直白的待办 store TodoStore,里面装着一个待办集合。暂时还没有 MobX 什么事儿。

我们刚刚创建了一个带有待办 todos 集合的 todoStore 实例。是时候往 TodoStore 里装些对象了。为了看到更改的效果,我们在每次更改后都调用 todoStore.report 把它打印出来。请注意,这个 report 会故意总是只打印第一个任务。这会让这个例子看起来有点不自然,但我们稍后会看到这样可以很好地展示出 MobX 依赖追踪的动态性。

变成响应式

到目前为止,这段代码并没有什么特别之处。但如果我们不是非得刻意调用 report,而是可以规定它必须在每次 相关 状态改变时都被调用呢?那样我们就不需要从代码中所有 有可能 对 report 产生影响的地方手动调用 report 了。我们确实想让最新的 report 被打印出来,但并不想因为要手动去组织它而费功夫。

幸运的是,这正是 MobX 能为我们做到的事情——自动执行只依赖于状态的代码。这样我们的 report 函数就可以自动更新,就像电子表格里的一个图表一样。为了做到这一点,TodoStore 就必须变成 observable,这样 MobX 就可以对所有正在进行的更改进行追踪。为了实现这一点,我们来改一下这个类。

还有,completedTodosCount 属性可以从待办清单中被自动派生出来。通过使用 observablecomputed 注解,我们可以为一个对象引入 observable 属性。在下面的例子中,我们为了刻意地展示出注解而使用了 makeObservable,但我们也可以改用 makeAutoObservable(this) 来简化这个过程。

就是这样!我们把有些属性标记为 observable,以便告诉 Mobx 这些值会随时间的推移而改变。有些值中的 computed 用来标明这些值能够从状态中被派生出来,而且只要底层状态没有发生改变,那么它们还可以从缓存中被派生出来。

我们目前还没有用到 pendingRequestsassignee 属性,但是会在这个教程后面的部分用到的。

在构造函数中,我们创建了一个打印 report 的小函数并把它用 autorun 包装了起来。而 autorun 会创建一个 action,这个 action 会先自动运行一次,之后每当小函数用到的任意一个 observable 数据发生了改变,它也会自动重新运行。report 会因为使用了 observable 属性 todos 而适时打印出 report。这一点会在下列代码中展示出来。只需要你按下 运行代码 按钮:

很有意思,对吧?report 的确自动同步而顺畅地打印出来了。如果你仔细查看打印出来的日志,你会看到第五行代码最后并没有再触发一行日志。这是因为 report 实际上 并没有因为第五行代码的重命名而发生改变——尽管它背后的数据变了。另一方面,更改第一个待办的名称确实更新了 report,因为 report 正使用着那个新名字。这很好地证明了 autorun 不仅监视观察着 todos 数组,还监视着待办条目中的各个属性。

把 React 变成响应式

好的,目前为止我们把一个傻傻的 report 变成了响应式。是时候围绕着这同一个 store 构建一个响应式的用户界面了。React 组件本身并不是响应式的(他们的名字除外)。mobx-react-lite 包中的 observer 高阶组件包装器通过(简而言之)把 React 组件用 autorun 包装起来解决了这个问题。这样可以自动让组件和状态相同步。这跟我们刚才对于 report 的做法在概念上并没有什么不同。

接下来这段代码定义了几个 React 组件。唯一 MobX 专用的代码是 observer 包装器。这就足够让每个组件在相关数据发生改变时单独重新渲染了。我们不用再调用状态的 useState setter 方法了,也不用去弄清楚怎样通过使用选择器或者需要手动配置的高阶组件来追踪应用状态中正确的那一部分。简而言之,所有组件都变得智能了。但他们是以一种傻瓜式、声明式的方式被定义出来的。

按下 运行代码 按钮,查看以下代码的实际运行状况。代码是可编辑的所以尽管动手玩起来。比如说,试试移除所有 observer 调用,或者只移除那个装饰在 TodoView 上的。右边预览中的数字突出显示了每个组件的渲染频率。

接下来的代码很好地证明了我们只需要更改数据,而不需要做更多的数据记录。MobX 会重新从 store 里的状态中自动派生并更新用户界面中相关的部分。

 

引用的使用

到目前为止我们创建了 observable 对象(原型对象和普通对象)、数组和原始值。你可能会好奇,MobX 是怎么处理引用的呢?我的状态可以组成图结构吗?在之前的代码中你可能已经注意到了每个待办对象中都有一个 assignee 属性。我们通过引入另一个装着若干人员的 “store” (好吧,只是一个被授予了非凡荣耀的数组)来给它们一些值,并把任务分配给它们。

我们现在有了两个独立的 store。一个装着人员,一个装着待办。刚才,为了把人员 store 中的人赋给 assignee,我们使用了一个引用。这些改变会被 TodoView 自动识别出来。有了 MobX,你就不需要先对数据进行归一化处理和手写选择器来保证组件的更新。其实,数据存放在哪儿并不重要。只要对象被转化成了 observable,MobX 就能对它们进行追踪。用真正的 JavaScript 引用就行。只要它们跟一个 derivation 有关,MobX 就会自动对它们进行追踪。如果想进行测试,就试着在下面的输入框内更改你的名字(要事先确定你之前已经按过了上面的 运行代码 按钮!)


你的名字:


顺便说一句,上面输入框的 HTML 也就是

<input onkeyup="peopleStore[1].name = event.target.value" />
这么简单而已。

异步 actions

既然我们小小待办应用中的一切都是从状态中派生出来的,那么状态在 什么时候 发生改变其实并不重要。这一点会让创建异步 actions 变得很简单。想模拟异步加载新待办的过程就按下下面的按钮吧(多按几次)。



这背后的代码真的很简单。我们首先更新 store 中的 pendingRequests 属性好让 UI 反映出当前正在加载的状态。一旦加载完成,我们就更新 store 中的待办事项并减小 pendingRequests 计数器的数值。请比较一下这段代码和之前 TodoList 的定义,看看 pendingRequests 属性是如何被使用的。

请注意,延时函数被包装在了 action 里。我们并不是非得这样做,但这样可以用一个 transaction 把两个变动都处理掉,让所有 observer 都只有在两个更新都完成后才收到通知。

observableTodoStore.pendingRequests++;
setTimeout(action(() => {
  observableTodoStore.addTodo('随机待办 ' + Math.random());
  observableTodoStore.pendingRequests--;
}), 2000);

总结

就这些了! 没有样板代码。只是用了一些组成我们完整的 UI 的简单声明式组件。而且它们全部都是从我们的状态中被响应式地派生出来的。现在你已经准备好在你自己的应用程序中使用 mobxmobx-react-lite 包了。对你目前所学到的东西做一个小结:

  1. 使用 observable 装饰器或 observable(对象或数组) 函数让对象能够被 MobX 追踪到。
  2. computed装饰器可以用来创建能够从状态中自动派生值并缓存它们的函数。
  3. 使用 autorun 来自动运行依赖于某些 observable 状态的函数。这对于记录日志、进行网络请求等都很有用。
  4. 使用 mobx-react-lite 包中的 observer 包装器让你的 React 组件做到真正的响应式。它们将会自动而高效地进行更新——即使是在数据量大的大型复杂应用中使用。

你可以随意用上面的可编辑代码块多玩一会儿,以便对于 MobX 对你所有更改的反应方式有个大致的感觉。比如,你可以在 report 函数中添加一个日志语句,看看它会在什么时候被调用。或者直接不展示 report,看看那样会对 TodoList 渲染造成怎样的影响。又或者只在某些特定情况下才把它展示出来……

MobX 并不支配架构

请注意,以上的例子都是刻意为之;推荐还是采用正确的工程实践,比如将逻辑封装到方法里、在 stores 里对它们进行组织,或采用 MVC(模型-视图-控制器)架构等等。你可以采用很多不同的架构模式,对于其中的某些模式,官方文档中有更详细的讨论。以上以及官方文档中的例子展示的都是我们 可以 如何使用 MobX,而不是 必须 如何使用。或者,如同 HackerNews 上的某个人所说的:

“MobX,我在别的地方已经提过了,但我还是忍不住要为它唱赞歌。用 MobX 写代码意味着使用 controllers、dispatchers、actions、supervisors 或另一种管理数据流的形式回归成了一种架构上的考虑——你可以根据自己应用的需要来采用架构模式——而不是编写待办应用之外所有的东西都默认必须要做的事情。”

React 预览

读下去并按下你遇到的 运行 按钮!


控制台打印