1

我正在寻找有关处理在不可变数据中使用 id/ref 链接所产生问题的方法的输入(特别是使用 Scala-js 和 scala-js-react 的 React,但我认为这些解决方案可能对任何人都通用类似的系统,例如 javascript 中的 React,或其他反应系统)。

这个概念是在我的数据中使用 aRef[A]代替 an A,其中将从数据中的多个位置引用同一项目。

Ref[A]将至少包含一个Id[A]允许以某种方式查找数据的内容 - 稍后会提供更多详细信息。

这允许在一个地方更新数据,然后仍然可以从其他地方引用该更新版本。

为了描述这个问题,我们可以从一个没有引用的不可变数据模型的系统开始——在这种情况下,我可以知道如果数据 x 和 y 的两个版本相等,则数据中没有任何变化。我们可能有类似的东西Post(User("Alice", "alice@example.com"), "Hello World")- 所有数据都包含在模型中,我们可以将不同Post的 s 作为普通数据进行比较。

我们还可以使用镜头来浏览数据,并且始终适用相同的逻辑。

然而,用户的电子邮件完全有可能在未来发生变化,我们可能不想留下散落在我们所有帖子中的过时电子邮件,或者必须更新帖子的数据以匹配新电子邮件。为此,我们可以引入 Refs。

为了更新这个例子,我们可以引入一个Ref[A]id 字段,其中包含Id[A]A 类型的引用对象。我们现在有了Post(Ref(Id(42)), "Hello World"), 和User(Id(42), "Alice", "alice@example.com")。要显示 Post,我们需要(以某种方式)跟随 Ref 从 Post 到 User 实例。如果 Alice 的电子邮件地址发生变化,Post 数据中的任何内容都不会发生变化——我们仍然只有一个 Ref(42)。

这在一方面是好的——当我们遵循 Ref 时,我们将获得新数据。然而,对于像 React 这样的系统来说,这是一个问题,我们需要通过比较模型的旧版本和新版本来判断模型何时发生了变化。我们一般会将数据模型作为 Props 的一部分进行传递,然后使用相等性比较新旧 props,看看它们是否发生了变化。当用户的电子邮件发生变化时,这将导致 Post 的呈现完全不改变。

对此的解决方案似乎很明显,将完整的引用数据集包含在Cache提供Id[A] => Option[A]. 然后,当任何 Id 引用的数据发生更改时,它将被新的 Cache 替换。这将被视为数据模型的一部分,并与它一起传递,例如作为(A, Cache). 我们仍然可以在 A 上使用镜头,并保持相同的缓存。当 Cache 发生变化时,(A, Cache)也会发生变化,让我们再次看到变化。我们确保我们遵守 React 组件的契约(至少是那些没有状态的组件),即渲染的输出只需要在 Props 发生变化时发生变化。

这里的问题是,(A, Cache)每当缓存中的任何项目发生变化时,都会发生变化,并且我们可能在缓存中拥有与任何给定组件无关的各种数据。这为我们提供了所需的更改,但也有许多无意义的更改导致浪费的重新渲染。

我觉得可能有更好的方法来处理这个问题,但我还没有找到任何东西。有没有解决这个问题的一般方法?

4

1 回答 1

1

与此同时,我能想到的最好的方法是扩展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),它只会查看Ain (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 操作:

  1. 将新数据项添加到缓存中。更新了 refs 的数据被添加到缓存中,并且将 referTo 字段设置为我们找到的 refs。然后,我们还会更新引用添加的任何其他数据项中的 refs。
  2. 更新缓存中的数据项。数据在存储之前已经更新了 refs,并且 referTo 字段更新为新的 refs。然后,我们还会更新引用更新后的任何其他数据项中的 refs。
  3. 当一个数据项引用另一个数据项时,该数据项被添加到缓存中或在缓存中更新。这不会增加该数据项的修订号,因此不会导致引用遍历数据的数据的传递更新。这与我们将在他们到达的数据发生变化时更新 Refs 的合同相匹配,而不是当需要遵循 2 个或更多 Refs 来查看更改时。

如果我们使用 React,当数据项更新或引用更新时,在上述任何一种情况下,都会重新渲染。这允许 React 组件注意到数据或参考修订的更改以触发重新渲染。

如上所述,我们只增加修订以显示对数据项本身内容的更改,而不是通过引用从它到达的数据项。因此,如果我们更改 Post 的消息,它将增加修订。如果我们更改 Post 的 userRef 中的 Id,这将再次增加修订。但是,如果用户更改,则帖子的修订版不会更改。重写 Post 以便 userRef 具有更新的修订版不会更新 Post 本身的修订版。

这意味着我们可以容忍循环引用,因为更新 refs 的修订不会触发进一步的更新。

为了看到这一点,假设我们在缓存中有 Post 和 User,如上所示。

  1. 我们希望更新用户以拥有一个新的电子邮件地址。这可以通过例如向缓存分派一个动作来请求这个来完成。
  2. 因为User已经更新了,遍历它,发现没有包含refs,所以没有做refs的重写。如果包含任何参考,它们将被更新。然后使用新用户更新缓存,该用户现在具有 Rev(1)。
  3. 缓存然后查找所有其他引用用户的数据项 - 这只是 Post。然后它遍历帖子并通过 Id(42) 找到用户的一个参考。此 ref 已更新为 User - Rev(1) 的当前版本。
  4. 此时,请注意,如果有任何引用 Post 的数据项,它们将不会被遍历 - 这将是一个“深度”更改而不是一个浅的更改,因此不会导致任何更新。例如,如果 User 包含对 Post 的(循环形成)引用,则不会导致无限更新循环。
  5. 缓存更新 Post 的条目 - 它仍然是 Rev(0),并且仍然引用 User,但有一个新的数据字段,其中包含重写的 Ref。此数据将传递给呈现 Post 的任何组件。
  6. 然后,我们期望渲染 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 状态会随着数据被检索而改变,然后随着它被服务器更新。我们将绑定到失败的查找以触发检索引用的数据,并且可能会定期清除最近未查找的数据。

遍历数据的要求一开始似乎有点烦人,但至少在某些系统中,这种遍历可以添加到用于编码/解码数据的类型类中,这些类型已经需要能够完全遍历数据模型。

我们还介绍了重写的必要性——然而这只是替换了数据模型的重写,这将需要更新嵌套在模型中而不是引用的数据(例如使用镜头),并且可以使用相同的镜头来工作。诚然,遍历整个数据模型比仅仅应用一个镜头效率要低,但希望遍历不会造成太大的负担。

于 2017-05-15T13:06:49.887 回答