Why immutable
Immutable,不可变的:对象一旦创建,就不能再改变;有什么用呢?让开发更简单。看下面的例子:
let params = {
a: 1
}
...
function fn1 (payload) {
payload.b = 2 // 修改了外面的params
...
}
逻辑需要我们在函数体内新增了属性b,同时无意中把外面的params也同时修改了;如果我们在外面的逻辑中需要依赖属性b,那我们可能很难判断b是在哪里修改的,这样引起的bug会很难修复;我们不应该在函数内修改传过来的参数,但你并没有规则真的禁止你这样做;实际上这样的代码很常见。结果就是,程序出了bug,我们可能花很长的时间寻找对象都在哪里被修改,无形中降低了开发效率。
另外一个好处是cache。现代的很多前端MVVM框架接管了将model渲染到view的工作,其中一个优化就是:如果model没有变,那么就不需要更新view。比如下面的例子:
let profile = {
name: 'Leo',
friends: [
{
id: 103742,
name: 'Cat'
},
...
]
}
profile.friends.push({id: 3243, name: 'Dog'})
render(profile)
function render(profile) {
// 这样是不行的
if (profile === _lastProfile)
return
...
}
我们希望当profile没有变化的时候,不重新渲染view,所以判断当前的profile与上一次的profile是否相等。我们知道这样是行不通的,因为在对象是可变的情况下,即使内容发生了变化,它们的引用还是相等,这就导致即使真的发生了变化,view也得不到更新。
好了,让我们再来看一下对象不可变的情况下,上面2个例子会是什么情况。
let params = {
a: 1
}
...
function fn1 (payload) {
payload = {...payload} // 或者Object.assign({}, payload)
payload.b = 2 // 不会修改外面的params
...
}
我们通过对象展开运算符新生成了一个对象,对新的对象进行操作,从而保证原对象没有发生变化。
let profile = {
name: 'Leo',
friends: [
{
id: 103742,
name: 'Cat'
},
...
]
}
let newProfile = {...profile}
newProfile.friends.push({id: 3243, name: 'Dog'})
render(newProfile)
function render(profile) {
// profile和_lastProfile不是同一个引用
if (profile === _lastProfile)
return
...
}
这回 ===
可以工作了。
性能
到这里我们可能有个疑问:每更新一次对象我们就要深拷贝一次,对象很大的时候性能会不会有问题?的确,当我们的对象包含很多属性时,比如 todos 对象包含 10000 个对象时,Object.assign的性能会有明显的下降:
let o = {}, s = {}
for (let i = 0; i < 100000; i++) {
o[i] = i
}
for (let i = 0; i < 10000; i++) {
o[i] = i
}
console.time('mutable 100000')
o['100001'] = 100001
console.timeEnd('mutable 100000')
console.time('Object.assign 100000')
let o2 = Object.assign({}, o)
o2['100001'] = 100001
console.timeEnd('Object.assign 100000')
console.time('mutable 10000')
s['10001'] = 10001
console.timeEnd('mutable 10000')
console.time('Object.assign 10000')
let s2 = Object.assign({}, s)
o2['10001'] = 10001
console.timeEnd('Object.assign 10000')
这里,我们分别对比直接修改、Object.assign
,对包含10000、100000属性的对象操作消耗的时间,结果如下:
可以看到,对于10000个属性的对象,Object.assign
性能虽有所下降,但降的并不多;当属性达到100000时,有明显的下降。
那么,怎么才能达到既要immutable,又要性能呢?
immutable-js
immutable-js是Facebook开源的一款实现了immutable特性的库,它通过巧妙的方式,在imutable和性能之间达到一种平衡: Persistent data structures
Persistent data structure
In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified. Such data structures are effectively immutable, as their operations do not (visibly) update the structure in-place, but instead always yield a new updated structure.
简单来说,在做拷贝时,并不需要拷贝所有的属性,只需要拷贝发生变化的那些,不变的那些还可以被重用,这就是 structure sharing。
比如,我们有这样一个对象:
const data = {
to: 7,
tea: 3,
ted: 4,
ten: 12,
A: 15,
i: 11,
in: 5,
inn: 9
}
我们用 [字典树Trie] (https://en.wikipedia.org/wiki/Trie) 这种数据结构保存它:
对于属性获取,比如要获取属性 ten
, 只需要按如下顺序 t -> e -> n
查找,即可找到值 12.
对于属性更新呢,比如 data.tea = 14
, 我们要新建一颗树,注意观察,我们并不需要新建所有的结点,只需要新建 root, t, e, a,即可,其余的都可以重用:
注意绿色的结点,就是需要新建的结点。
旧的结点可以很快被GC回收:
这就是Immutable.Map的基本原理:每个结点有32个子结点,每个子结点为0表示尚未设值,为1表示有值,查找时先对属性进行hash,通过选择合适的hash函数,可以保证hash值在[0, 2^32 -1]之间,通过与11111做与运算,可以快速地确定属性应该放在哪一位上;如果当前位已经有值,那么就下钻一层,同时把hash值右移5位,重复同样的操作,直到找到的那个位置还没有设值,则把值放在那个位置。
实际上当属性个数小于8个时,只用一个数组来保存各个属性。
总结
Immutable可以在一定程度上简化我们的开发,特别是与redux结果使用效果更佳。但是不是一定要用Immutable-js呢?未必。当数据量不大时,用 Object.assin
可以达到同样的效果,性能也没有太大的下降。当数据量很大时,immutable-js 可以很大程度上提高性能。但同时,它也带来一些不便:只能用immutable-js提供的数据结构。