Immutable.js 关系

IT技术 javascript reactjs immutable.js
2021-05-11 07:56:04

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

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,但我不确定它们是否是解决方案。如果可能的话,你能给我举个例子吗?

4个回答

转述问题:

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

简答

不。

长答案

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

更长的答案

比较复杂...

不变性

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

结果

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

  • 之间不存在差异通过噪声值通过噪声参考语义
  • 一些比较可以更容易
  • 当你在某处传递一个对象的引用时,你不必担心代码的其他部分会改变它
  • 当你从某个地方得到一个对象的引用时,你知道它永远不会改变
  • 你避免了一些并发问题,因为没有时间变化的概念
  • 当没有任何变化时,您不必担心更改是否是原子性的
  • 使用不可变数据结构更容易实现软件事务内存(STM)

但世界是可变的

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

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

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

如何应对变化

那么如何使用不可变数据对变化进行建模呢?Clojure 中使用VarsRefsAtomsAgents解决了一个很好的方法,并且ClojureScript(一个面向 JavaScript 的 Clojure 编译器)支持其中的一些(特别是 Atoms 应该像在 Clojure 中一样工作,但有没有 Refs、STM、Vars 或 Agents - 看看ClojureScript 和 Clojure 在并发特性方面有什么区别)。

看看Atoms 在 ClojureScript 中是如何实现的,您似乎可以使用普通的 JavaScript 对象来实现相同的功能。它适用于诸如拥有本身可变的 JavaScript 对象之类的事情,但它有一个属性是对不可变对象的引用——您将无法更改该不可变对象的任何属性,但您将能够构造一个不同的不可变对象,并将旧的对象交换到顶级可变对象中的新对象。

其他像Haskell这样纯函数式的语言可能有不同的方式来处理可变世界,比如monad(一个众所周知的难以解释的概念——Douglas Crockford,JavaScript: The Good Parts 的作者JSON 的发现者将其归因于“monadic 诅咒”在他的谈话中单子和性腺)。

你的问题看似简单,其实涉及的问题很复杂。当然,如果对您的问题回答“否”是否可以在一个对象发生变化时自动更新所有相关对象,那当然会错过这一点,但它比这更复杂,并说在不可变对象中没有任何变化(所以这个问题永远不会发生)同样无济于事。

可能的解决方案

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

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

如果您总是使用data.value(或使用一些更好的名称)访问您的数据,那么您可以将 传递data给代码的其他部分,并且每当您的状态发生变化时,您只需为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})
])};

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

一些阅读

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

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

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

其他方法

有关不变性的另一种方法,即使用不可变对象作为不一定恒定的数据的代理,请参阅Yegor Bugayenko 和他的文章不可变对象与常识网络研讨会:

Yegor Bugayenko 使用术语“不可变”的含义与函数式编程上下文中通常的含义略有不同。他不使用不可变或持久的数据结构和函数式编程,而是提倡使用面向对象编程的原始意义,即您实际上从未真正改变任何对象,但您可以要求它改变某些状态,或改变某些数据,它本身被认为是与对象分开的。很容易想象一个与关系数据库对话的不可变对象。对象本身可以是不可变的,但它仍然可以更新存储在数据库中的数据。很难想象存储在 RAM 中的某些数据可以被认为与数据库中的数据一样与对象分开,但实际上并没有太大区别。

如果有什么需要澄清的,请发表评论。

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

这是一个示例 react/flux-ish 应用程序,演示了处理此问题的一种方法。这里的关键是数字 ID 用于引用而不是 Javascript 引用。

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

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

这种情况可能可以通过使用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'});

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

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