【问题标题】:Immutable.js relationshipsImmutable.js 关系
【发布时间】:2014-12-12 15:34:35
【问题描述】:

假设约翰有两个孩子爱丽丝和鲍勃,鲍勃有一只猫猎户座。

var Immutable = require('immutable');

var parent = Immutable.Map({name: 'John'});
var childrens = Immutable.List([
    Immutable.Map({name: 'Alice', parent: parent}),
    Immutable.Map({name: 'Bob', parent: parent})
]);
var cat = Immutable.Map({name: 'Orion', owner: childrens.get(1)});

几年后,约翰想改名为简。

var renamedParent = parent.set('name', 'Jane');

...并让孩子们知道。

childrens = childrens.map(function(children) {
    children.set('parent', renamedParent);
});

然后我必须更新 cat,因为 Bob 变了。

cat = cat.set('owner', childrens.get(1));

当一个对象发生变化时,是否可以自动更新所有相关对象?我查看了Cursors,但我不确定它们是否是一个解决方案。如果可以的话,能给我举个例子吗?

【问题讨论】:

    标签: javascript reactjs immutable.js


    【解决方案1】:

    解释问题:

    是否可以在不可变集合中的一个对象发生变化时自动更新所有相关对象?

    简答

    没有。

    长答案

    没有,但在不可变数据结构中没有任何变化,所以这不是问题。

    更长的答案

    这更复杂......

    不变性

    不可变对象的全部意义在于,如果您有对不可变对象的引用,则不必费心检查其任何属性是否已更改。那么,这不是问题吗?嗯……

    后果

    这会产生一些后果 - 它们是好是坏取决于您的期望:

    • pass-by-valuepass-by-reference 语义没有区别
    • 有些比较可能更容易
    • 当您在某处传递对某个对象的引用时,您不必担心代码的其他部分会改变它
    • 当您从某个地方获得对某个对象的引用时,您知道它永远不会改变
    • 您可以避免一些并发问题,因为没有时间变化的概念
    • 当没有任何变化时,您不必担心变化是否是原子的
    • 使用不可变数据结构实现software transactional memory (STM) 更容易

    但世界是可变的

    当然,在实践中,我们经常处理随时间变化的值。不可变状态似乎无法描述可变世界,但人们有一些处理方式。

    这样看:如果您在某个 ID 上有您的地址并且您搬到了另一个地址,则应该更改该 ID 以与新的真实数据保持一致,因为您居住在该地址不再是真实的.但是,当您购买包含您的地址的东西时收到发票,然后您更改了地址,则发票保持不变,因为您在开具发票时居住在该地址仍然是真实的。现实世界中的一些数据表示是不可变的,例如该示例中的发票,而有些则是可变的,例如 ID。

    现在以您的示例为例,如果您选择使用不可变结构来对数据建模,则必须以您考虑我示例中的发票的方式来考虑它。数据可能不是最新的,但它在某个时间点始终是一致和真实的,并且永远不会改变。

    如何应对变化

    那么如何使用不可变数据对变化进行建模?在Clojure 中使用VarsRefsAtomsAgentsClojureScript(针对JavaScript 的Clojure 编译器)有一个很好的解决方法)支持其中一些(在特定的 Atom 应该像在 Clojure 中一样工作,但没有 Refs、STM、Vars 或 Agent - 请参阅 what are the differences between ClojureScript and Clojure regarding concurrency features)。

    how Atoms are implemented in ClojureScript 似乎你可以使用普通的JavaScript 对象来实现相同的功能。它适用于诸如拥有一个本身可变的 JavaScript 对象,但它具有一个对不可变对象的引用的属性 - 您将无法更改该不可变对象的任何属性,但您将能够构造一个不同的不可变对象,并将旧对象交换为顶级可变对象中的新对象。

    Haskellpurely functional 这样的其他语言可能有不同的方式来处理可变世界,比如monads(一个众所周知的难以解释的概念-JavaScript: The Good Partsdiscoverer of JSON 的作者Douglas Crockford 将其归因于它在他的演讲中提到“一元诅咒”Monads and Gonads)。

    你的问题看起来很简单,但它所涉及的问题实际上相当复杂。当然,对您的问题是否可以在一个对象发生更改时自动更新所有相关对象时回答“否”会失去意义,但它比这更复杂,并且说在不可变对象中什么都不会改变(所以这个问题永远不会发生)同样没有帮助。

    可能的解决方案

    您可以拥有一个顶级对象或变量,您始终可以从中访问所有结构。假设您有:

    var data = { value: Immutable.Map({...}) }
    

    如果您总是使用data.value(或使用更好的名称)访问您的数据,那么您可以将data 传递给代码的其他部分,并且每当您的状态发生变化时,您只需分配一个新的 Immutable.Map ({...}) 到您的data.value,此时您所有使用data 的代码都将获得新值。

    如何以及何时将 data.value 更新为新的不可变结构可以通过使其从用于更新状态的 setter 函数自动触发来解决。

    另一种方法是在每个结构级别使用类似的技巧,例如 - 我使用变量的原始拼写:

    var parent = {data: Immutable.Map({name: 'John'}) };
    var childrens = {data: Immutable.List([
        Immutable.Map({name: 'Alice', parent: parent}),
        Immutable.Map({name: 'Bob', parent: parent})
    ])};
    

    但是您必须记住,您所拥有的值不是不可变结构,而是那些引用不可变结构的附加对象,这些对象引入了额外的间接级别。

    一些阅读

    我的建议是查看其中一些项目和文章:

    • Immutable React Peter Hausel 的文章
    • Morearty.js - 在 React 中使用不可变状态,就像在 Om 中一样,但用纯 JavaScript 编写
    • react-cursor - 用于 Facebook 的功能状态管理抽象
    • Omniscient - 为 React 组件提供抽象的库,允许快速自上而下渲染包含不可变数据 * Om - React 的 ClojureScript 接口
    • mori - 在 JavaScript 中使用 ClojureScript 的持久数据结构的库
    • Fluxy - Facebook 的 Flux 架构的实现
    • Facebook's Immutable - 用于 JavaScript 的持久数据集合,当与 Facebook ReactFacebook Flux 结合使用时会遇到类似的问题(搜索如何将 Immutable 与 React 和 Flux 结合可能会给你一些好主意)

    我希望这个答案即使没有给出一个简单的解决方案也会有所帮助,因为用不可变结构描述可变世界是一个非常有趣且重要的问题。

    其他方法

    有关不可变性的不同方法,不可变对象代表不一定恒定的数据,请参阅 Yegor Bugayenko 的 Immutable Objects vs. Common Sense 网络研讨会和他的文章:

    Yegor Bugayenko 使用术语“不可变”的含义与它在函数式编程上下文中的通常含义略有不同。他不使用不可变或持久的数据结构和函数式编程,而是提倡使用原始意义上的面向对象编程,即您永远不会真正改变任何对象,但您可以要求它改变一些状态,或者改变一些数据,它本身被认为是与对象分离的。很容易想象一个与关系数据库对话的不可变对象。对象本身可以是不可变的,但它仍然可以更新存储在数据库中的数据。很难想象存储在 RAM 中的某些数据可以被认为与数据库中的数据一样独立于对象,但实际上并没有太大区别。如果您将对象视为公开某些行为的自治实体,并且尊重它们的抽象边界,那么将对象视为不同于数据的东西实际上是很有意义的,然后您可以拥有具有可变数据的不可变对象。

    如果有任何需要澄清的地方,请发表评论。

    【讨论】:

      【解决方案2】:

      正如 rsp 所澄清的,这个问题反映了不变性提供的内在权衡。

      这里的an example react/flux-ish app 演示了一种解决此问题的方法。这里的关键是数字 ID 用于引用而不是 Javascript 引用。

      【讨论】:

        【解决方案3】:

        据我了解,您使用的不可变对象可能有点太高级了。据我所知,不可变对象对于表示时间捕获状态很有用。所以很容易比较 state1 === state2。但是,在您的示例中,您在该州也有时间捕获的关系。将完整状态与另一个完整状态进行比较很好,因为它是不变的数据,所以它非常可读,但因此也很难更新。我建议在您添加另一个库来解决此问题之前,尝试看看您是否可以在较低级别实现 Immutable,您实际上需要比较时间捕获的状态。例如,要知道 1 个实体(人/猫)是否发生了变化。

        因此,如果您确实想使用不可变对象,请使用它们。但是如果你想要动态对象,不要使用不可变对象。

        【讨论】:

          【解决方案4】:

          这种情况可能可以通过使用ids 引用父母而不是父母记录本身来解决。我在这里寻求 SQL (sorta) 的指导:一条记录​​不应该有指向内存中另一条记录的指针,而应该知道另一条记录的 id

          在我的应用程序中,我喜欢让每个对象只知道其引用对象的主键,这样我就不必担心对象发生变化会怎样等问题。只要它的密钥保持不变(应该如此),我总能找到它:

          var Immutable = require('immutable');
          
          var people = Immutable.OrderedMap({
              0: Immutable.Map({name: 'John', id: '0'}),
              1: Immutable.Map({name: 'Alice', id: '1', parentId: '0'}),
              2: Immutable.Map({name: 'Bob', id: '2', parentId: '0'})
          });
          var cat = Immutable.Map({name: 'Orion', ownerId: '2'});
          

          现在我可以使用标准的immutable.js 工具更改任何记录的任何特征(当然id 除外),而无需进行任何额外的更新。

          people = people.set('0', people.get('0').set('name', 'Jane'))
          

          【讨论】:

            【解决方案5】:

            在对同样的问题感到沮丧并且无处可去之后,我决定将关系完全移出我的对象并管理各种常见关联的逻辑(一对一、一对多、多对-many) 分开。

            本质上,我正在使用双向地图管理关系,并将关系逻辑集中在一个对象中,而不是像传统情况那样集中在我的每个建模数据中。这意味着我需要想出逻辑来观察,例如,当父级发生更改并将更改传播到子关系(因此是 bimaps)时。然而,由于它在自己的模块中被抽象出来,因此可以重复使用来模拟未来的关系。

            我决定不使用 ImmutableJS 来处理这个问题,而是使用我自己的库 (https://github.com/jameslk/relatedjs) 并使用 ES6 功能,但我想它可以以类似的方式完成。

            【讨论】:

              猜你喜欢
              • 2019-05-18
              • 2018-02-17
              • 2018-07-06
              • 1970-01-01
              • 2017-05-21
              • 2014-11-10
              • 2016-10-04
              • 2021-07-01
              • 1970-01-01
              相关资源
              最近更新 更多