使用immutable.js相对于Object.assign或扩展运算符的优点

28

到目前为止,我看到的大多数“入门脚手架”和一些有关react / redux的帖子都鼓励使用immutable.js来解决可变性问题。我个人依赖Object.assign或展开运算符来处理这个问题,因此实际上并没有看到immutable.js的优势,因为它增加了额外的学习成本,并且需要从用于可变性的vanilla js技术中转移。我试图找到切换的有效原因,但没有成功,因此在这里询问为什么它如此流行。

6个回答

32

这一切都关乎效率。

持久化数据结构

持久化数据结构在被改变时会保留先前的版本,始终产生一个新的数据结构。为了避免昂贵的克隆,只存储与先前数据结构的差异,而交集则在它们之间共享。这种策略称为结构共享。因此,持久化数据结构比使用Object.assign或展开运算符进行克隆要高效得多。

Javascript中持久化数据结构的缺点

不幸的是,Javascript没有本地支持持久化数据结构。这就是immutable.js存在的原因,它的对象与普通的Javascript Object有很大的区别。这导致代码更冗长,并且需要将持久化数据结构转换为本地Javascript数据结构。

关键问题

当immutable.js的结构共享(效率)何时超过其缺点(冗长、转换)?

我猜这个库只在具有大量和广泛的对象和集合的大型项目中回报,当整个数据结构的克隆和垃圾收集变得更加昂贵时。


谢谢你的回答,但是能否像对待一个笨蛋一样向我解释一下?到目前为止,我理解了Immutable.js使用持久化数据结构来存储原始数组或对象中未更改的部分。但是你仍然需要将未更改的部分和更改的部分分配给变量,那么它为什么比Object.assign()array.slice()更有效呢?Immutable.js执行哪些内部操作以实现更少的内存使用? - codeepic

16
我已经为多个不可变库创建了性能基准,脚本和结果位于immutable-assign (GitHub项目)中,该项目显示immutable.js针对写操作进行了优化,比Object.assign()更快,但在读操作方面较慢。以下是基准测试结果的摘要:
-- Mutable
Total elapsed = 50 ms (read) + 53 ms (write) = 103 ms.

-- Immutable (Object.assign)
Total elapsed = 50 ms (read) + 2149 ms (write) = 2199 ms.

-- Immutable (immutable.js)
Total elapsed = 638 ms (read) + 1052 ms (write) = 1690 ms.

-- Immutable (seamless-immutable)
Total elapsed = 31 ms (read) + 91302 ms (write) = 91333 ms.

-- Immutable (immutable-assign (created by me))
Total elapsed = 50 ms (read) + 2173 ms (write) = 2223 ms.

因此,是否使用immutable.js取决于您的应用程序类型以及读写比率。如果您有大量的写操作,则immutable.js将是一个不错的选择。

过早优化是万恶之源

理想情况下,在引入任何性能优化之前,您应该对应用程序进行分析,但是,不可变性是那些设计决策之一,必须尽早决定。当您开始使用 immutable.js时,需要在整个应用程序中使用它以获得性能优势,因为使用fromJS()和toJS()与普通JS对象进行交互非常昂贵。


4
我认为ImmutableJs的主要优势在于其数据结构和速度。当然,它也强制不可变性,但你应该一直这样做,所以这只是一个附加好处。
例如,假设你的reducer中有一个非常大的对象,并且你想更改该对象的一小部分。由于不可变性,你不能直接更改对象,而必须创建一个对象的副本。您可以通过复制所有内容(在ES6中使用展开运算符)来实现此目的。
那么问题是什么?复制非常大的对象非常慢。 ImmutableJS中的数据结构执行一种称为“结构共享”的操作,其中您仅真正更改所需的数据。您未更改的其他数据在对象之间共享,因此不会被复制。
这样做的结果是具有快速写入的高效数据结构。
ImmutableJS还为深层对象提供了简单的比较。例如:
const a = Immutable.Map({ a: Immutable.Map({ a: 'a', b: 'b'}), b: 'b'});
const b = Immutable.Map({ a: Immutable.Map({ a: 'a', b: 'b'}), b: 'b'});

console.log(a.equals(b)) // true 

没有这个,你需要一些深度比较函数,而这也会花费很多时间。然而,在这里根节点包含整个数据结构的哈希(不要引用我说的话,但比较总是瞬间完成),因此比较始终是O(1),即瞬间完成,无论对象大小如何。

这在React的shouldComponentUpdate方法中特别有用,您可以使用此equals函数仅比较props,它会立即运行。

当然,也有缺点,如果将immutablejs结构和常规对象混合使用,则很难分辨出哪个是哪个。此外,您的代码库中充斥着与常规Javascript不同的immutableJS语法。

另一个缺点是,如果您不打算使用深度嵌套的对象,那么它会比普通的js慢一些,因为数据结构确实具有一些开销。

以上是我的个人见解。


使用扩展运算符(Object.assign)创建的副本不是浅拷贝吗?就像我期望的那样,let obj = {listWith200MillionsObjects: [...], username: 'Kevin'}; let newObj= {...obj, username: 'Pavlin'};只是复制了对200百万个对象列表的引用,而不是每个200百万个对象都进行复制?并且,相比较而言,它是否也能快速进行比较呢?obj == newObj会返回false,因为它们是两个不同的对象,所以只是指针的比较?尽管let newObj2 = {...obj};会使得newObj2 == obj返回false,即使它们包含相同的数据。 - Kevin Doyon

2
immutable.js的优势在于它强制实施了一个不可变的redux存储(否则您可能会忘记扩展或保护存储免受修改),并且与Object.assign或展开运算符相比大大简化了减速器(这两者都是浅层次的!)。
话虽如此,我曾在一个大型Angular 1.x项目中使用过redux+immutable,存在一些缺点:性能问题,不清楚什么是不可变的,不能在ng-repeat中使用不可变的immutable.js结构等。不确定React是否一样,很想听听意见。

您还可以使用 Object.freeze()deep-freeze 来强制实现纯 JS 对象的不可变性。 - engineforce
@engineforce seamless-immutable 就是做这个的。但在生产环境中,他们会关闭它。 - oligofren

1
这不仅仅是通过持久化数据结构获得性能收益。不可变性本身就是一种非常理想的品质,因为它完全消除了由于意外突变而引起的任何错误。此外,在Javascript中具有方便API的合理数据结构是很好的,实际可用的东西,即使在ES6和ES7最近的添加之后,immutable.js也远远领先于vanilla对象和数组。就像jQuery一样——它非常受欢迎,因为它的API真的非常好,易于使用,与vanilla DOM API(简直是噩梦)相比。当然,您可以尝试使用vanilla对象保持不可变,但是这样您就必须每次都记得使用Object.assign和array spread,而使用immutable.js,您就绝对不会出错,这里的任何突变都是显式的。

1

immutable.js为您提供了许多实用工具,总是返回一个新对象。

例如:

var Immutable = require('immutable');
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50

对于仅使用React / Redux,我个人认为Object.assign已经足够强大了,但在某些情况下,使用该库可以节省您几行代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接