与不可变数据中的引用进行反应

时间:2017-05-15 13:06:50

标签: scala reactjs scala.js scalajs-react

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

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

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

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

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

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

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

要更新示例,我们可以为类型A的引用引入一个{id}字段包含Ref[A]的{​​{1}}。我们现在有Id[A]Post(Ref(Id(42)), "Hello World") 。要显示帖子,我们需要(以某种方式)遵循从帖子到用户实例的参考。如果Alice的电子邮件地址发生变化,Post数据中没有任何内容发生变化 - 我们仍然只有Ref(42)。

这在某种程度上是好的 - 当我们遵循参考时,我们将获得新数据。然而,对于像React这样的系统来说,这是一个问题,我们需要通过比较模型的旧版本和新模型来判断模型何时发生变化。我们通常将数据模型作为Props的一部分传递,然后使用相等性来比较新旧道具,看它们是否已经改变。当用户的电子邮件发生变化时,这将导致Post的呈现完全没有变化。

对此的解决方案似乎是显而易见的,将全套引用数据包含在提供User(Id(42), "Alice", "alice@example.com")的不可变Cache之类的内容中。当任何Id引用的数据发生更改时,将使用新的Cache替换它。这将被视为数据模型的一部分,并传递给它,比如Id[A] => Option[A]。我们仍然可以在A上使用镜头,并保持相同的缓存。当缓存更改时,(A, Cache)也会更改,从而允许我们再次查看更改。我们确保遵守React Components的合约(至少那些没有状态的合约),渲染的输出只需要在Props改变时改变。

这里的问题是,只要缓存中的任何项发生变化,(A, Cache)就会发生变化,我们可能会在缓存中拥有与任何给定组件无关的各种数据。这使我们得到了必要的改变,但也有许多无意义的改变导致浪费的重新渲染。

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

1 个答案:

答案 0 :(得分:1)

与此同时,我能想到的最佳方法是扩展Ref[A],使其不仅包含Id[A],还包含我们所引用数据的特定修订版。基本上,我们将引用的数据建模为时间序列 - 数据在每个修订版本中都有一个值,该值永远不会改变,我们只能引用特定的修订版本。修订例如是case class Rev(r: Int)。然后,如果数据模型包含Ref(Id(42), Rev(0)),则它专门引用ID为42的数据的修订版0。如果它继续引用此修订版,则Ref不会更改,但引用的数据也不会。为了查看引用数据的新修订版,我们需要将Ref更新为新版本。这样做会改变Ref本身,因此整个数据也会发生变化 - 这反过来可以被React检测到。修订是相当随意的 - 它们只需要在数据更改时增加,它们可以特定于每个数据项或整体计数器。

对Refs的这一改变反过来允许我们从比较中排除Cache,以决定是否重新渲染数据,摆脱我们不参考的数据的浪费渲染。我们知道,如果通过遵循单个引用可以访问的任何引用数据发生更改,则数据本身将会更改。

请注意,这在跟随2次或更多次跳转时没有帮助 - 但是这可以相对容易地处理,例如在React中我们只需要确保每次我们遵循引用来获取新数据时,我们都会通过子组件的新数据道具。这允许React'"检测"更改该数据并触发重新呈现。然后子组件可以跟随进一步的引用,再次将查找的数据传递给它自己的子组件,我们可以通过任意数量的引用来递归。

我们仍然会将Cache传递给所有组件,但是他们不需要使用它来检测更改,只需要查找引用的数据。在React中,我们可以提供一个shouldComponentUpdate函数(或可重用性),它只会查看A中的(A, Cache)

管理所需的refs重写将由中央系统处理(例如在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。然后,我们还会更新引用添加的任何其他数据项中的引用。
  2. 更新缓存中的数据项。数据在存储之前已经更新,并且referTo字段更新为新的refs。然后,我们还会更新任何引用更新的数据项的引用。
  3. 当数据项引用另一个数据项时,该数据项将添加到缓存中或在缓存中更新。这不会增加该数据项的修订号,因此不会导致引用遍历数据的数据的传递更新。这与我们将在一步到达的数据发生变化时更新Refs的合同相匹配,而不是在需要遵循2个或更多Refs来查看更改时。
  4. 如果我们使用React,当更新数据项或更新引用时,在上述情况之一中,它将被重新呈现。这允许React组件注意到对数据或ref refvisions的更改以触发重新呈现。

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

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

    要看到这个有效,我们假设我们在缓存中有Post和User,如上所述。

    1. 我们希望更新用户以获得新的电子邮件地址。例如,可以通过将操作分派给Cache来请求此操作来完成此操作。
    2. 因为用户已经更新,所以它被遍历并发现不包含引用,因此不会重写refs。如果包含任何引用,它们将被更新。然后使用新用户更新缓存,该用户现在具有Rev(1)。
    3. 然后缓存会查找引用该用户的所有其他数据项 - 这只是Post。然后它遍历帖子并通过Id(42)向用户找到一个引用。此引用已更新为User-Rev(1)的当前版本。
    4. 此时,请注意,如果有任何数据项引用了Post,则不会遍历它们 - 这将是" deep"改变不浅,因此不会导致任何更新。例如,如果User包含一个(循环形成)引用,则不会导致无限更新循环。
    5. 缓存更新Post的条目 - 它仍然在Rev(0),仍然引用User,但是有一个带有重写Ref的新数据字段。此数据将传递给呈现Post的任何组件。
    6. 然后我们期望重新呈现的组件呈现Post说明直接保留在帖子中的消息,并从Cache中查找用户。然后必须通过props将此用户传递给子组件进行渲染。由于子组件具有用户作为其道具,因此它也将重新呈现,因为React允许将旧用户与新用户进行比较。如果呈现Post的组件只是直接呈现User,则在这种情况下它将起作用,因为userRef将在浅层方式更改User数据时更新。但是,如果用户包含Ref,我们就不会在Post组件中看到对Ref的目标的更改。
    7. 这个系统似乎应该扩展到服务器 - 客户端使用,我们可以使用类似Diode的Pot系统的东西来表示从服务器检索的数据。而不是Id[A] => Option[A]我们会Id[A] => Pot[A],而特定的Pot状态会随着数据的检索而改变,然后由服务器更新。我们将绑定到失败的查找以触发检索引用的数据,并可能定期清除最近未查找过的数据。

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

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