Recoil将原子模型引入了React世界。它的新功能是以陡峭的学习曲线和稀缺的学习资源为代价的。
Jotai和Zedux后来简化了这个新模型的各个方面,提供了许多新功能并突破了这个惊人的新范式的极限。
其他文章将重点介绍这些工具之间的区别。本文将重点介绍这三者的一个共同特点:
他们修复了 Flux。
如果您不了解 Flux,这里有一个快速要点:
除了Redux之外,所有基于 Flux 的库基本上都遵循这种模式:一个应用程序有多个商店。只有一个 Dispatcher 的工作是以正确的顺序向所有商店提供操作。这种“正确的顺序”意味着动态地整理商店之间的依赖关系。
例如,采用电子商务应用程序设置:
当用户将一根香蕉移动到他们的购物车时,PromosStore 需要等待 CartStore 的状态更新,然后再发送请求以查看是否有可用的香蕉优惠券。
或者香蕉可能无法运送到用户所在的区域。 CartStore 在更新之前需要检查 UserStore。或者优惠券一周只能使用一次。 PromosStore 在发送优惠券请求之前需要检查 UserStore。
Flux 不喜欢这些依赖项。来自遗留的 React 文档:
Flux 应用程序中的对象是高度解耦的,并且非常严格地遵守迪米特法则,即系统中的每个对象应该尽可能少地了解系统中的其他对象的原则。
这背后的理论是可靠的。 100%。 Soo……这家Flux的多店口味怎么就死了?
事实证明,孤立状态容器之间的依赖是不可避免的。事实上,为了保持代码模块化和 DRY,您应该经常使用其他商店。
在 Flux 中,这些依赖项是即时创建的:
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
这个例子展示了如何不在商店之间直接声明依赖关系——相反,它们是在每个操作的基础上拼凑在一起的。这些非正式的依赖关系需要挖掘实现代码才能找到。
这是一个非常简单的例子!但是你已经可以看到 Flux 的狼狈样了。副作用、选择操作和状态更新都拼凑在一起。这种托管实际上有点不错。但是混入一些非正式的依赖项,将配方增加三倍,并在一些样板文件中提供它,您会发现 Flux 很快就会崩溃。
Flummox和Reflux等其他 Flux 实现改进了样板和调试体验。虽然非常有用,但依赖管理是困扰所有 Flux 实现的一个棘手问题。使用另一家商店感觉很丑陋。深层嵌套的依赖树很难遵循。
这个电子商务应用程序有一天可能会拥有 OrderHistory、ShippingCalculator、DeliveryEstimate、BananasHoarded 等商店。一个大型应用程序很容易拥有数百家商店。您如何使每个商店的依赖项保持最新?你如何追踪副作用?纯度呢?调试呢?香蕉真的是浆果吗?
至于 Flux 引入的编程原则,单向数据流是赢家,但目前,得墨忒耳定律还不是。
我们都知道Redux是如何大放异彩来挽救局面的。它放弃了多个商店的概念,转而采用单例模式。现在一切都可以在没有任何“依赖性”的情况下访问其他一切。
Reducers 是纯粹的,所以所有处理多个状态切片的逻辑都必须在存储之外。社区制定了管理副作用和派生状态的标准。 Redux 商店具有出色的可调试性。 Redux 最初未能修复的唯一主要 Flux 缺陷是它的样板。
RTK后来简化了 Redux 臭名昭著的样板文件。然后Zustand以一些调试能力为代价去除了一些绒毛。所有这些工具在 React 世界中都变得非常流行。
对于模块化状态,依赖树自然会变得如此复杂,以至于我们能想到的最佳解决方案是,“我猜就别这么做了。”
它奏效了!这种新的单例方法对于大多数应用程序仍然适用。 Flux 原则非常可靠,只需删除依赖噩梦就可以解决它。
还是做到了?
单例方法的成功引出了一个问题,Flux 最初的目的是什么?为什么我们想要多家商店?
请允许我阐明这一点。
对于多个商店,状态片段被分解到它们自己的自治、模块化容器中。这些商店可以单独进行测试。它们也可以在应用程序和包之间轻松共享。
这些自治存储可以拆分为单独的代码块。在浏览器中,它们可以延迟加载并即时插入。
Redux 的 reducer 也很容易进行代码拆分。感谢replaceReducer
,唯一的额外步骤是创建新的组合减速器。但是,当涉及到副作用和中间件时,可能需要更多的步骤。
使用单例模型,很难知道如何将外部模块的内部状态与您自己的集成。 Redux 社区引入了 Ducks 模式来尝试解决这个问题。它可以工作,但需要一些样板文件。
对于多个商店,外部模块可以简单地暴露一个商店。例如,表单库可以导出 FormStore。这样做的好处是标准是“官方的”,这意味着人们不太可能创建自己的方法。这导致了一个更健壮、统一的社区和包生态系统。
单例模型的性能令人惊讶。 Redux 已经证明了这一点。然而,其选择模型尤其具有硬性上限。我在这个 Reselect discussion中写了一些关于这个的想法。一个大而昂贵的选择器树可能真的开始拖延,即使在对缓存进行最大控制时也是如此。
另一方面,对于多个存储,大多数状态更新都被隔离到状态树的一小部分。他们不接触系统中的任何其他东西。这是可扩展的,远远超出单例方法——事实上,对于多个商店,很难在用户机器上达到内存限制之前达到 CPU 限制。
在 Redux 中销毁状态并不太难。就像在代码拆分示例中一样,它只需要几个额外的步骤就可以删除 reducer 层次结构的一部分。但是对于多个商店来说它仍然更简单 - 理论上,您可以简单地将商店与调度程序分离并允许它被垃圾收集。
这是 Redux、Zustand 和一般的单例模型处理不好的大问题。副作用与它们相互作用的状态是分开的。选择逻辑与一切分离。虽然多商店 Flux 可能过于集中,但 Redux 却走向了相反的极端。
有了多个自主商店,这些东西自然会结合在一起。确实,Flux 只是缺少一些标准来防止一切都变成乱七八糟的官话大杂烩(抱歉)。
现在,如果您了解 OG Flux 库,您就会知道它实际上在所有这些方面都不是很好。调度程序仍然采用全局方法 - 将每个操作调度到每个商店。具有非正式/隐式依赖关系的整个事情也使代码拆分和破坏变得不那么完美。
尽管如此,Flux 还是有很多很酷的功能。此外,多存储方法具有更多功能的潜力,例如控制反转和分形(又名本地)状态管理。
如果没有人将他们的女神命名为得墨忒尔,Flux 可能会演变成一个真正强大的状态管理者。我是认真的! ... 好吧,我不是。但既然你提到了它,也许得墨忒尔定律值得仔细研究一下:
这个所谓的“法”到底是什么?来自维基百科:
- 每个单元对其他单元的了解应该有限:只有与当前单元“密切”相关的单元。
- 每个单元应该只和它的朋友交谈;不要和陌生人说话。
该法则在设计时考虑了面向对象编程,但它可以应用于许多领域,包括 React 状态管理。
基本思想是防止商店:
用香蕉术语来说,一根香蕉不应该剥另一根香蕉,也不应该和另一棵树上的香蕉说话。但是,如果两棵树先架设一根香蕉电话线,它就可以与另一棵树通话。
这鼓励关注点分离并帮助您的代码保持模块化、DRY 和 SOLID。扎实的理论!那么 Flux 缺少什么?
好吧,商店间的依赖关系是一个好的模块化系统的自然组成部分。如果商店需要添加另一个依赖项,它应该这样做并尽可能明确地这样做。下面是一些 Flux 代码:
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore 有多个以不同方式声明的依赖项——它等待并从CartStore
读取,它从UserStore
读取。发现这些依赖项的唯一方法是在 PromosStore 的实现中查找商店。
开发工具也无法帮助使这些依赖关系更容易被发现。换句话说,依赖关系太隐含了。
虽然这是一个非常简单和做作的例子,但它说明了 Flux 是如何误解得墨忒耳法则的。虽然我确信它主要是出于保持 Flux 实现较小的愿望(真正的依赖管理是一项复杂的任务!),但这正是 Flux 的不足之处。
不同于这个故事的英雄:
2020 年, Recoil应运而生。虽然一开始有点笨拙,但它教会了我们一种新模式,使 Flux 的多商店方法复活。
单向数据流从商店本身移动到依赖图。商店现在被称为原子。原子是适当自治和代码可分割的。他们拥有新的力量,如悬念支持和水合作用。最重要的是,原子正式声明它们的依赖关系。
原子模型诞生了。
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil 与臃肿的代码库、内存泄漏、糟糕的性能、缓慢的开发和不稳定的功能——最显着的副作用——作斗争。它会慢慢解决其中的一些问题,但与此同时,其他图书馆采用了 Recoil 的想法并与它们一起运行。
Jotai突然出现并迅速获得了追随者。
// a Jotai atom const greetingAtom = atom('Hello, World!')
除了 Recoil 大小的一小部分外,Jotai 还提供了更好的性能、更时尚的 API,并且由于其基于 WeakMap 的方法而没有内存泄漏。
然而,它是以一些功能为代价的——WeakMap 方法使缓存控制变得困难,并且几乎不可能在多个窗口或其他领域之间共享状态。缺少字符串键,虽然很漂亮,但使调试成为一场噩梦。大多数应用程序应该重新添加这些内容,从而大大损害 Jotai 的流畅性。
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
一些荣誉提名是Reatom和Nanostores 。这些库探索了更多原子模型背后的理论,并试图将其规模和速度推向极限。
原子模型很快并且扩展性很好。但直到最近,还有一些原子库没有很好解决的问题:
学习曲线。原子不同。我们如何让 React 开发人员能够理解这些概念?
Dev X 和调试。我们如何让原子被发现?您如何跟踪更新或执行良好做法?
现有代码库的增量迁移。您如何访问外部商店?您如何保持现有逻辑不变?你如何避免完全重写?
插件。我们如何使原子模型可扩展?它能处理所有可能的情况吗?
依赖注入。原子自然地定义了依赖关系,但它们可以在测试期间或在不同的环境中被换掉吗?
得墨忒耳法则。我们如何隐藏实现细节并防止分散更新?
这是我进来的地方。看,我是另一个原子库的主要创建者:
几周前, Zedux终于进入了现场。 Zedux 由纽约的一家金融科技公司开发——我工作的公司——Zedux 不仅旨在快速和可扩展,而且还提供流畅的开发和调试体验。
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
我不会在这里深入探讨 Zedux 的特性——正如我所说,本文不会重点介绍这些原子库之间的区别。
只要说 Zedux 解决了上述所有问题就够了。例如,它是第一个提供真正的控制反转的原子库,也是第一个通过提供用于隐藏实现细节的原子导出将我们带回得墨忒耳法则的完整循环。
Flux 的最后意识形态终于复活了——不仅复活了,而且得到了改进! - 感谢原子模型。
那么究竟什么是原子模型呢?
这些原子库有很多差异——它们甚至对“原子”的含义有不同的定义。普遍的共识是,原子是小型的、孤立的、自治的状态容器,通过有向无环图进行反应更新。
我知道,我知道,这听起来很复杂,但等我用香蕉解释一下。
我在开玩笑!其实很简单:
通过图形更新弹跳。就是这样!
关键是,无论实现或语义如何,所有这些原子库都恢复了多个存储的概念,并使它们不仅可用,而且使用起来非常愉快。
我给出的想要多个商店的 6 个理由正是原子模型如此强大的原因:
简单的 API 和可扩展性本身就使原子库成为每个 React 应用程序的绝佳选择。比 Redux 更强大、样板更少?这是梦吗?
多么美妙的旅程! React 状态管理的世界总是令人惊奇,我很高兴搭上了顺风车。
我们才刚刚开始。原子有很大的创新空间。在花费数年时间创建和使用 Zedux 之后,我已经看到了原子模型的强大之处。事实上,它的强大之处在于它的致命弱点:
当开发人员探索原子时,他们经常深入挖掘各种可能性,以至于他们回来说,“看看这疯狂的复杂力量”,而不是“看看原子是如何简单而优雅地解决这个问题的。”我是来改变这一切的。
原子模型及其背后的理论并没有以大多数 React 开发人员可以接受的方式进行教授。在某种程度上,到目前为止,React 世界对原子的体验与 Flux 正好相反:
本文是我制作的一系列学习资源中的第二篇,旨在帮助 React 开发人员了解原子库的工作原理以及您可能想要使用原子库的原因。查看第一篇文章 -可扩展性:React 状态管理的丢失级别。
花了 10 年时间,但由于原子模型,Flux 引入的坚实的 CS 理论最终在很大程度上影响了 React 应用程序。在未来的几年里,它将继续这样做。