与此同时,我能想到的最好的方法是扩展Ref[A]
,使其不仅包含Id[A]
我们所引用的数据的一个特定版本,而且还包含一个特定的版本。本质上,我们将引用的数据建模为时间序列 - 数据在其自身的每个修订版中都有一个值,该值永远不会改变,我们只能引用一个特定的修订版。例如,修订case class Rev(r: Int)
。那么如果数据模型包含一个Ref(Id(42), Rev(0))
它专门引用了 id 为 42 的数据的修订版 0。如果它继续引用此修订版,则 Ref 不会更改,但引用的数据也不会更改。为了查看引用数据的新版本,我们需要将 Ref 更新到该新版本。这样做会改变 Ref 本身,从而改变整个数据——这反过来可以被 React 检测到。修订是相当随意的 - 它们只需要在数据更改时增加,它们可以特定于每个数据项或整体计数器。
对 Refs 的这种更改反过来允许我们从比较中排除缓存,以决定是否重新渲染数据,从而摆脱对我们不引用的数据的浪费渲染。我们知道,如果通过单个引用可以访问的任何引用数据发生变化,则数据本身也会发生变化。
请注意,这在跟随 2 次或更多跳转时无济于事 - 但是这可以相对容易地处理,例如在 React 中,我们只需要确保每次跟随引用以获取新数据时,我们将新数据传递给子组件的道具。这使得 React 可以“检测”对数据的更改并触发重新渲染。然后子组件可以跟随更多的引用,再次将查找到的数据传递给它自己的子组件,我们可以通过任意数量的引用进行递归。
我们仍然会将缓存传递给所有组件,但它们不需要使用它来检测更改,只需查找引用的数据。在 React 中,我们可以提供一个 shouldComponentUpdate 函数(或 Reusability),它只会查看A
in (A, Cache)
。
管理所需的 ref 重写将由中央系统处理(例如,在 React 中,这可能是具有所有数据缓存的根组件)。它将提供从 id 到相关数据的映射,但还会在必要时管理重写该数据,以便它包含每个引用的最新修订,然后将此数据传递给子组件以进行渲染。
要更新示例,我们现在需要所有顶级数据项的 Id。使用 scala-ish 伪代码,我们的缓存内容有点像:
Id(1) -> (data = Post(id = Id(1), userRef = Ref(Id(42), Rev(0)), message = "Hello World!"), rev = Rev(0), refersTo = Set(Id(42)))
Id(42) -> (data = User(id = Id(42), name = "Alice", email = "alice@example.com"), rev = Rev(0), refersTo = Set())
因此,对于每个 Id,我们都有数据项本身、数据项所在的修订版以及它引用的一组其他数据项。我们只存储最新的修订 - 如果查找旧的修订将解析为无。
然后缓存有一个函数 updateRefs 遍历一个数据项,寻找 Refs。这将生成一个更新的数据项,其中包含最新版本的所有参考,以及在数据中找到的一组参考。
我们在以下情况下运行此 updateRefs 操作:
- 将新数据项添加到缓存中。更新了 refs 的数据被添加到缓存中,并且将 referTo 字段设置为我们找到的 refs。然后,我们还会更新引用添加的任何其他数据项中的 refs。
- 更新缓存中的数据项。数据在存储之前已经更新了 refs,并且 referTo 字段更新为新的 refs。然后,我们还会更新引用更新后的任何其他数据项中的 refs。
- 当一个数据项引用另一个数据项时,该数据项被添加到缓存中或在缓存中更新。这不会增加该数据项的修订号,因此不会导致引用遍历数据的数据的传递更新。这与我们将在他们到达的数据发生变化时更新 Refs 的合同相匹配,而不是当需要遵循 2 个或更多 Refs 来查看更改时。
如果我们使用 React,当数据项更新或引用更新时,在上述任何一种情况下,都会重新渲染。这允许 React 组件注意到数据或参考修订的更改以触发重新渲染。
如上所述,我们只增加修订以显示对数据项本身内容的更改,而不是通过引用从它到达的数据项。因此,如果我们更改 Post 的消息,它将增加修订。如果我们更改 Post 的 userRef 中的 Id,这将再次增加修订。但是,如果用户更改,则帖子的修订版不会更改。重写 Post 以便 userRef 具有更新的修订版不会更新 Post 本身的修订版。
这意味着我们可以容忍循环引用,因为更新 refs 的修订不会触发进一步的更新。
为了看到这一点,假设我们在缓存中有 Post 和 User,如上所示。
- 我们希望更新用户以拥有一个新的电子邮件地址。这可以通过例如向缓存分派一个动作来请求这个来完成。
- 因为User已经更新了,遍历它,发现没有包含refs,所以没有做refs的重写。如果包含任何参考,它们将被更新。然后使用新用户更新缓存,该用户现在具有 Rev(1)。
- 缓存然后查找所有其他引用用户的数据项 - 这只是 Post。然后它遍历帖子并通过 Id(42) 找到用户的一个参考。此 ref 已更新为 User - Rev(1) 的当前版本。
- 此时,请注意,如果有任何引用 Post 的数据项,它们将不会被遍历 - 这将是一个“深度”更改而不是一个浅的更改,因此不会导致任何更新。例如,如果 User 包含对 Post 的(循环形成)引用,则不会导致无限更新循环。
- 缓存更新 Post 的条目 - 它仍然是 Rev(0),并且仍然引用 User,但有一个新的数据字段,其中包含重写的 Ref。此数据将传递给呈现 Post 的任何组件。
- 然后,我们期望渲染 Post 的组件重新渲染直接保存在帖子中的消息,并从缓存中查找用户。然后它必须通过 props 将此 User 传递给子组件以进行渲染。由于子组件将 User 作为其 Props,因此它也会重新渲染,因为 React 允许将旧 User 与新 User 进行比较。如果渲染 Post 的组件只是直接渲染了 User,那么它会在这种情况下工作,因为只要 User 数据以浅层方式更改,userRef 就会更新。但是,如果 User 包含一个 Ref,我们将不会在 Post 组件中看到对该 Ref 目标的更改。
这个系统似乎也应该扩展到服务器-客户端使用,我们可以使用类似于 DiodePot
系统的东西来表示从服务器检索的数据。而不是Id[A] => Option[A]
我们有的Id[A] => Pot[A]
,特定的 Pot 状态会随着数据被检索而改变,然后随着它被服务器更新。我们将绑定到失败的查找以触发检索引用的数据,并且可能会定期清除最近未查找的数据。
遍历数据的要求一开始似乎有点烦人,但至少在某些系统中,这种遍历可以添加到用于编码/解码数据的类型类中,这些类型已经需要能够完全遍历数据模型。
我们还介绍了重写的必要性——然而这只是替换了数据模型的重写,这将需要更新嵌套在模型中而不是引用的数据(例如使用镜头),并且可以使用相同的镜头来工作。诚然,遍历整个数据模型比仅仅应用一个镜头效率要低,但希望遍历不会造成太大的负担。