MobX
是一款简单、可扩展且经实战检验的 状态管理解决方案。本教程将在十分钟内教你 MobX 的所有重要概念。MobX 是一个独立的库,但大多数人都在与 React 一起使用它,本教程侧重于这种组合。
核心思想
状态是每个应用程序的核心,没有什么比产生不一致的状态或与周围存在的局部变量不同步的状态更快地创建出有错误、难以管理的应用程序了。因此,许多状态管理解决方案试图限制修改状态的方式,例如通过使状态不可变。但这又带来了新问题;数据需要被规范化,引用完整性不再能得到保证,而且在你想使用类等强大概念时,它变得几乎不可能使用。
MobX 通过解决根本问题,使状态管理再次变得简单:它使产生不一致的状态成为不可能。实现这一目标的策略很简单:确保所有可以从应用程序状态中推导出来的东西都会被推导出来。自动地。
从概念上讲,MobX 将您的应用程序视为一个电子表格。
- 首先,是应用程序状态。对象、数组、基本类型、引用图,它们构成了应用程序的模型。这些值是应用程序的“数据单元”。
- 其次是推导。基本上,任何可以从应用程序状态自动计算出来的值。这些推导,或计算值,可以从简单的值(如未完成待办事项的数量)到复杂的东西(如待办事项的视觉 HTML 表示)。用电子表格术语来说:这些是应用程序的公式和图表。
- 反应与推导非常相似。主要区别在于这些函数不产生值。相反,它们自动运行以执行某些任务。通常,这与 I/O 相关。它们确保 DOM 在正确的时间被更新,或者网络请求在正确的时间被自动发出。
- 最后是操作。操作是改变状态的所有事情。MobX 将确保由您的操作导致的应用程序状态的所有更改都会被所有推导和反应自动处理。同步且无故障。
一个简单的待办事项存储...
理论够多了,实际操作可能比仔细阅读以上内容解释得更多。为了原创,让我们从一个非常简单的待办事项存储开始。请注意,下面所有的代码块都是可编辑的,所以请使用运行代码按钮来执行它们。下面是一个非常简单的TodoStore
,它维护着待办事项的集合。还没有涉及 MobX。
我们刚刚创建了一个带有todos
集合的todoStore
实例。现在需要用一些对象来填充 todoStore
。为了确保我们看到了更改的效果,我们在每次更改后都调用了todoStore.report
并记录它。请注意,报告故意总是只打印第一个任务。这使得这个例子有点人为,但正如我们稍后将看到的那样,它很好地证明了 MobX 的依赖项跟踪是动态的。
变得响应式
到目前为止,这段代码没有什么特别之处。但如果我们不必显式地调用report
,而是可以声明在每次相关状态更改时都应该调用它呢?这样就能让我们免去在代码库中任何可能影响报告的地方调用report
的责任。我们确实希望确保打印最新的报告。但我们不想被组织这件事所困扰。
幸运的是,这正是 MobX 可以为我们做的。自动执行仅依赖于状态的代码。这样,我们的report
函数就会像电子表格中的图表一样自动更新。为了实现这一点,TodoStore
必须变得可观察,以便 MobX 可以跟踪所有正在进行的更改。让我们改变一下类,使其足以实现这一点。
此外,completedTodosCount
属性可以从待办事项列表中自动推导出来。通过使用observable
和computed
注解,我们可以在对象上引入可观察的属性。在下面的例子中,我们使用makeObservable
来显式地显示注解,但我们也可以使用makeAutoObservable(this)
来简化这个过程。
就是这样!我们标记了一些属性为observable
,以向 MobX 信号这些值可能会随着时间的推移而发生变化。计算用computed
装饰,以标识它们可以从状态中推导出来,并且只要没有基础状态发生变化,就会被缓存。
pendingRequests
和assignee
属性目前还没有使用,但在本教程的后面会使用。
在构造函数中,我们创建了一个打印report
的小函数,并将其包装在autorun
中。Autorun 创建了一个反应,该反应运行一次,然后在函数内部使用的任何可观察数据发生变化时自动重新运行。由于report
使用了可观察的todos
属性,因此它将在适当的时候打印报告。这在下一段代码中得到了证明。只需按下运行按钮即可。
纯乐趣,对吧?report
确实自动、同步地打印出来了,而且没有泄漏中间值。如果你仔细查看日志,你会发现第五行没有产生新的日志行。因为报告并没有实际改变,尽管后备数据发生了变化。另一方面,改变第一个待办事项的名称确实更新了报告,因为该名称被报告积极地使用。这很好地证明了autorun
不仅观察了todos
数组,还观察了待办事项项目中的各个属性。
使 React 变得响应式
好的,到目前为止,我们已经使一个愚蠢的报告变得响应式。现在是时候在这个相同的存储库周围构建一个响应式用户界面了。React 组件(尽管它们的名字)本身并不响应式。mobx-react-lite
包中的observer
HoC 包装器通过将 React 组件包装在autorun
中来修复了这个问题。这使组件与状态保持同步。从概念上讲,这与我们之前对report
所做的没有区别。
下一段代码定义了几个 React 组件。其中唯一与 MobX 相关的代码是observer
包装。这足以确保每个组件在相关数据发生变化时单独重新渲染。我们不再需要调用状态useState
设置器,也不需要弄清楚如何使用选择器或需要配置的高阶组件来订阅应用程序状态的正确部分。基本上,所有组件都变得智能了。但它们是以一种愚蠢的、声明性的方式定义的。
按下运行代码按钮,看看下面的代码在行动。这段代码是可编辑的,所以请随意玩耍。例如,尝试删除所有observer
调用,或者只删除装饰TodoView
的调用。右侧预览中的数字突出显示了组件渲染的频率。
下一段代码很好地证明了我们只需要改变数据,而不需要做任何进一步的簿记工作。MobX 将自动从存储中的状态再次推导出并更新用户界面的相关部分。
处理引用
到目前为止,我们已经创建了可观察的对象(原型对象和普通对象)、数组和基本类型。你可能想知道,MobX 如何处理引用?我的状态可以形成图吗?在之前的代码段中,你可能已经注意到,待办事项上有一个assignee
属性。让我们通过引入另一个“存储”(好吧,它只是一个美化的数组)来包含人,并将任务分配给他们,来给它们一些值。
现在我们有两个独立的存储库。一个包含人,一个包含待办事项。要将assignee
分配给来自人员存储库的人员,我们只需分配一个引用。这些更改将被TodoView
自动拾取。使用 MobX,不需要先规范化数据,也不需要编写选择器来确保我们的组件得到更新。事实上,数据存储在哪里并不重要。只要对象被标记为可观察的,MobX 就能够跟踪它们。真正的 JavaScript 引用将直接工作。如果它们与推导相关,MobX 将自动跟踪它们。为了测试这一点,只需尝试在下一个输入框中更改你的姓名(确保你先按下了上面的运行代码按钮!)。
你的姓名
顺便说一下,上面输入框的 HTML 代码很简单
<input onkeyup="peopleStore[1].name = event.target.value" />
异步操作
由于我们小型待办事项应用程序中的所有内容都来自状态,因此状态何时更改并不重要。这使得创建异步操作变得非常简单。只需按下以下按钮(多次)即可模拟异步加载新的待办事项项目。
背后的代码非常简单。我们首先更新存储库属性pendingRequests
,以便 UI 反映当前的加载状态。加载完成后,我们更新存储库的待办事项,并将pendingRequests
计数器再次减少。只需将这段代码与之前的TodoList
定义进行比较,看看pendingRequests
属性是如何使用的。
请注意,超时函数被包装在action
中。这不是严格必要的,但它可以确保两个突变在一个事务中被处理,确保所有观察者只在两个更新都完成后才被通知。
observableTodoStore.pendingRequests++; setTimeout(action(() => { observableTodoStore.addTodo('Random Todo ' + Math.random()); observableTodoStore.pendingRequests--; }), 2000);
结论
就是这样!没有样板代码。只是一些简单的、声明性的组件,它们形成了我们完整的 UI。它们完全、响应式地从我们的状态中推导出。现在你就可以在自己的应用程序中开始使用mobx
和mobx-react-lite
包了。你迄今为止所学内容的简短总结。
- 使用
observable
装饰器或observable(object or array)
函数使对象可被 MobX 跟踪。 computed
装饰器可用于创建可以自动从状态中推导出值并对其进行缓存的函数。- 使用
autorun
自动运行依赖于某些可观察状态的函数。这对于记录、发出网络请求等非常有用。 - 使用
mobx-react-lite
包中的observer
包装器使你的 React 组件真正地响应式。它们将自动高效地更新。即使在使用大量数据的庞大复杂应用程序中也是如此。
随意玩耍更长的时间,使用上面的可编辑代码块,以获得对 MobX 如何对所有更改做出反应的基本了解。例如,你可以向report
函数中添加一个日志语句,以查看它何时被调用。或者根本不显示report
,看看它如何影响TodoList
的渲染。或者,你可以在特定情况下显示它...
MobX 不规定架构
请注意,以上示例是人为编造的,建议使用适当的工程实践,例如将逻辑封装在方法中,将它们组织在存储库或控制器、视图模型等中。可以应用许多不同的架构模式,其中一些将在官方文档中进一步讨论。以上示例和官方文档中的示例展示了 MobX 如何可以使用,而不是它如何必须使用。或者,正如 HackerNews 上的人所说
“MobX,它在别处被提到过,但我忍不住要歌颂它。在 MobX 中编写意味着使用控制器/调度器/操作/主管或其他形式的管理数据流又回到了一个架构问题,你可以根据应用程序的需要对其进行模式化,而不是默认情况下,除了待办事项应用程序以外,任何其他应用程序都必须使用它。”