- 原文地址:Immutability in React: There’s nothing wrong with mutating objects
- 原文做者:Esteban Herrera
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:jonjia
- 校对者:MechanicianW goldEli
当开始使用 React 时,你要学习的第一件事就是不该该改变(修改)一个 数组:javascript
// bad, push 操做会修改原数组
items.push(newItem);
// good, concat 操做不会修改原数组
const newItems = items.concat([newItem]);
复制代码
可是html
你知道为何要这么作吗?前端
可变对象有什么不对吗?java
没什么不对的,真的。可变对象没有任何问题。react
固然,在涉及并发状况时会有问题。但这是最简单的开发方法,和编程中许多问题同样,这是一种折衷。android
函数式编程和 immutability 等概念很流行,都是很酷的主题。但就 React 而言,immutability 会给你一些实际的好处。不只仅是由于流行。而是有实用价值。ios
Immutability 表示通过一些处理后值或状态保持不变的变量。git
概念很简单,但深究起来并不简单。github
你能够在 JavaScript 语言自己中找到 immutable 类型。String
对象的值类型就是一个很好的例子。web
若是你声明一个字符串变量,以下:
var str = 'abc';
复制代码
你没法直接修改字符串中的字符。
在 JavaScript 中,字符串类型的值不是数组,因此你不能像下面这样作:
str[2] = 'd';
复制代码
能够试试这样:
str = 'abd';
复制代码
将另外一个字符串赋值给 str
。
你甚至能够将 str
从新声明为一个常量:
const str = 'abc'
复制代码
结果,从新声明会产生一个错误(可是这个错误和 immutability 无关)。
若是你想修改字符串的值,可使用字符串方法,例如:replace、toUpperCase 或 trim。
全部这些方法都会返回一个新的字符串,而不会改变原字符串的值。
可能你没注意到,以前我加粗强调过值类型。
字符串的值是 immutable(不可变的)。字符串对象就不是了。
若是一个对象是 immutable 的,你不能改变他的状态(及他的属性值)。也意味着不能给他添加新的属性。
试试下面的代码, 你能够在 JSFiddle 中查看
const str = "abc";
str.myNewProperty = "some value";
alert(str.myNewProperty);
复制代码
若是你运行他,会弹出一个 undefined
,
新的属性并无添加上。
但再试试下面这个:你能够在 JSFiddle 中查看
const str = new String("abc");
str.myNewProperty = "some value";
alert(str.myNewProperty);
str.myNewProperty = "a new value";
alert(str.myNewProperty);
复制代码
String 对象不是 immutable 的。
最后一个示例经过 String()
构造函数建立了一个字符串对象,他的值是 immutable 的。但你能够给这个对象添加新的属性,由于这是一对象而且没有被 冻结。
这就要求咱们理解另外一个重要概念。引用相等和值相等的不一样。
引用相等,你经过 ===
和 !==
(或者 ==
和 !=
) 操做符比较对象的引用。若是引用指向同一个对象,那他们就是相等的:
var str1 = ‘abc’;
var str2 = str1;
str1 === str2 // true
复制代码
在上面的例子中,两个引用(str1
和 str2
)都指向同一个对象('abc'
),因此他们是相等的。
若是两个引用都指向一个 immutable 的值,他们也是相等的,以下:
var str1 = ‘abc’;
var str2 = ‘abc’;
str1 === str2 // true
var n1 = 1;
var n2 = 1;
n1 === n2 // also true
复制代码
但若是指向的是对象,那就再也不相等了:
var str1 = new String(‘abc’);
var str2 = new String(‘abc’);
str1 === str2 // false
var arr1 = [];
var arr2 = [];
arr1 === arr2 // false
复制代码
上面的两种状况,都会建立两个不一样的对象,因此他们的引用不相等:
若是你想检查两个对象的值是否相等,你须要比较他们的值属性。
在 JavaScript 中,没有直接比较数组和对象值的方法。
若是你要比较字符串对象,可使用返回新字符串的 valueOf
或 trim
方法:
var str1 = new String(‘abc’);
var str2 = new String(‘abc’);
str1.valueOf() === str2.valueOf() // true
str1.trim() === str2.trim() // true
复制代码
但对于其余类型的对象,你只能实现本身的比较方法或者使用第三方工具,能够参考 这篇文章。
但这和 immutability 和 React 有什么关系呢?
若是两个对象是不可变的,那么比较他们是否相等比较容易。React 就是利用了这个概念来进行性能优化的。
咱们来具体谈谈吧。
React 内部会维护一份 UI 表述,就是 虚拟 DOM。
若是一个组件的属性和状态改变了,他对应的虚拟 DOM 数据也会更新这些变化。由于不用修改真实页面,操做虚拟 DOM 更加方便快捷。
而后,React 会对如今和更新前版本的虚拟 DOM 进行比较,来找出哪些改变了。这就是 一致性比较 的过程。
这样,就只有有变化的元素会在真实 DOM 中更新。
有时,一些 DOM 元素自身没变化,但会被其余元素影响,形成从新渲染。
这种状况下,你能够经过 shouldComponentUpdate 方法来判断属性和方法是否是真的改变了,是否返回 true 来更新这个组件:
class MyComponent extends Component {
// ...
shouldComponentUpdate(nextProps, nextState) {
if (this.props.myProp !== nextProps.color) {
return true;
}
return false;
}
// ...
}
复制代码
若是组件的属性和状态是 immutable 的对象或值,你能够经过相等比较判断他们是否改变了。
从这个角度看,immutability 下降了复杂度。
由于,有时候很难知道什么改变了。
考虑下面的深嵌套:
myPackage.sender.address.country.id = 1;
复制代码
如何跟踪是哪一个对象改变了呢?
再考虑下数组。
两个长度一致的数组,比较他们是否相等的惟一方式就是比较每一个元素是否都相等。对于大型数组,这样的操做消耗很大。
最简单的解决方法就是使用 immutable 对象。
若是须要更新一个对象,就用新的值建立一个新的对象,由于原对象是 immutable 的。
你也能够经过引用比较来肯定他有没有改变。
但对有些人来讲,这个概念可能与性能和代码简洁性方面的理念不一致。
那咱们来回顾下建立新对象并保证 immutability 的观点。
在实际应用中,state 和 property 多是对象或数组。
JavaScript 提供了一些建立这些数据新版本的方法。
对于对象,不是手动建立具备新属性的对象(以下):
const modifyShirt = (shirt, newColor, newSize) => {
return {
id: shirt.id,
desc: shirt.desc,
color: newColor,
size: newSize
};
}
复制代码
而是可使用 Object.assign 这个方法避免定义未修改的属性(以下):
const modifyShirt = (shirt, newColor, newSize) => {
return Object.assign( {}, shirt, {
color: newColor,
size: newSize
});
}
复制代码
Object.assign
方法用于将(从第二个参数开始)全部源对象的属性复制到第一个参数声明的目标对象。
或者你也可使用 扩展运算符 达到目的(不一样的是 Object.assign()
使用 setter 方法分配新的值,而扩展运算符不是,参考):
const modifyShirt = (shirt, newColor, newSize) => {
return {
...shirt,
color: newColor,
size: newSize
};
}
复制代码
对于数组,你也可使用扩展运算符建立具备新元素的数组:
const addValue = (arr) => {
return [...arr, 1];
};
复制代码
或者使用像 concat
或 slice
这样的方法返回一个新的数组,而不会修改原数组:
const addValue = (arr) => {
return arr.concat([1]);
};
const removeValue = (arr, index) => {
return arr.slice(0, index)
.concat(
arr.slice(index+1)
);
};
复制代码
在这个 代码片断 中,你能够看到在进行一些常见操做时,如何用这些方法结合扩展运算符避免修改原数组。
可是,使用这些方法会有两个主要缺点:
因为上述缘由,使用外部库来实现 immutability 是更好的选择。
React 团队推荐使用 Immutable.js 和 immutability-helper,但 这里 有不少一样功能的库。主要有下面三种类型:
大部分库都是配合 持久的数据结构 来工做。
当有些数据须要修改时,持久的数据结构会建立一个新的版本(这实现了数据的 immutable),同时提供全部版本的访问权限。
若是数据部分持久化,全部版本的数据均可以访问,但只有最新版能够修改。若是数据彻底持久化,那每一个版本均可以访问和修改。
基于树和共享的理念,新版本的建立很是高效。
数据结构表层是一个 list 或 map,但在底层是使用一种叫作 trie 的树来实现(具体来讲就是 位图向量 tire),其中只有叶节点存储值,二进制表示的属性名是内部节点。
好比,对于下面的数组:
[1, 2, 3, 4, 5]
复制代码
你能够将索引转化为 4 位的二进制数:
0: 0000
1: 0001
2: 0010
3: 0011
4: 0100
复制代码
将数组按下面的树形展现:
每一个层级都有两个字节造成到达值的路径。
如今若是咱们想将 1
修改成 6
:
不是直接修改树中的那个值,而是将从根节点到你要修改的那个值总体复制一份:
会在新复制的树中更新那个值:
原树中的其余节点能够继续使用:
也能够说,未修改的节点会被新旧两个版本共享。
固然,这些 4 位的树形并不普适于这些持久的数据结构。这只是结构共享的基本理念。
我不会介绍更多细节了,想了解更多关于持久化数据和结构共享的知识,能够阅读 这篇文章 和 这个演讲。
Immutability 也不是没有问题。
正如我前面提到的,处理对象和数组时,你要么必须记住使用保证 immutability 的方法,要么就使用第三方库。
但这些库大多都使用本身的数据类型。
尽管这些库提供了兼容的 API 和将这些类型转为 JavaScript 类型的方法,但在设计你本身的应用时,也要当心处理:
toJs()
这样有性能弊病的方法若是库没有实现新的数据结构(好比使用冻结对象工做的库),就不能体现结构共享的好处。极可能更新数据时要复制对象,有些状况性能会受到影响。
此外,你必须考虑这些库的学习曲线。
当须要选择 immutability 方案时,要仔细考虑。
也能够阅读下这篇文章 immutability 的反对观点。
Immutability 是 React 开发者须要理解的一个概念。
一个 immutable 的值或对象不能被改变,因此每次更新数据都会建立新的值,将旧版本的数据隔离。
例如,若是你应用的 state 是 immutable 的,就能够将全部 state 对象保存在单个 store 中,这样很容易实现撤销/重作功能。
听起来是否是很熟悉?是的。
像 Git 这种版本管理系统以相似方式工做。
可是,人们更关注 Redux 的 纯函数 和 应用状态的快照。StackOverflow 上的 这个回答 很好地解释了 Redux 和 immutability 的关系。
Immutability 还有其余像避免意外的反作用和 减小耦合 等优势,但也有缺点。
记住,和编程中许多事同样,这也是一种折衷。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。