Immutable 的理解及应用
JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。例如:
foo = { a: 1 }
bar = foo
bar.a = 2
foo.a // 2
可以看到,此时 foo.a
也被改成了 2。虽然这样做可以节约内存,但对复杂应用来说,是个非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用浅拷贝(shallowCopy)或深拷贝(deepCopy)来避免被修改,但这样做造成了 CPU 和内存的浪费。
而 Immutable 可以很好地解决这些问题:
// 原生写法
let foo = { a: { b: 1 } }
let bar = foo
bar.a.b = 2
console.log(foo.a.b) // 2
console.log(foo === bar) // true
// immutable 写法
import Immutable from 'immutable'
foo = Immutable.fromJS({ a: { b: 1 } })
bar = foo.setIn(['a', 'b'], 2) // setIn 赋值, getIn 取值
console.log(foo.getIn(['a', 'b'])) // 1
console.log(foo === bar) // false
一、Immutable 的定义
1、什么是 Immutable
Immutable 指一旦创建,就不能再被修改的数据,当对 Immutable 对象进行修改时,会返回一个新的 Immutable 对象,以此来保证数据的不可变。
2、Immutable 的优点
- 降低了 Mutable 带来的复杂度
可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。例如:
function touchAndLog(handleData) {
let data = { key: 'value' }
handleData(data)
console.log(data.key) // 未知的打印结果
}
上面由于不确定 handleData
的功能,无法确定 data.key 的打印结果,但如果 data 是 Immutable,则可以确定打印的就是 value。
- 节约内存
Immutable 使用了结构共享,会尽量复用内存,没有被引用的对象会被垃圾回收。例如:
import { Map } from 'immutable'
let a = Map({
select: 'users',
filter: Map({ name: 'Tim' })
})
let b = a.set('select', 'people')
a === b // false
a.get('filter') === b.get('filter') // true
上面 a 和 b 共享了没有变化的 filter
节点。
3、Immutable 的缺点
增加了资源文件大小
容易与原生对象混淆
Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作不同,例如要用 map.get('key')
而不是 map.key,array.get(0)
而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。当使用外部库时,一般需要使用原生对象,也很容易忘记转换。
可以通过以下办法来解决:
- 使用 Flow 或 TypeScript 等静态类型检查工具;
- 约定变量命名规则:如所有 Immutable 类型对象以
$$
开头。 - 使用
Immutable.fromJS
而不是 Immutable.Map 或 Immutable.List 来创建对象,这样可以避免 Immutable 和原生对象间的混用。
4、Immutable 的实现原理
Immutable 实现的原理是持久化数据结构(Persistent Data Structure):
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,新对象会尽可能利用之前的数据结构,避免对内存造成浪费。
也就是说,使用旧数据创建新数据时,要保证旧数据同时可用且不变,为了避免深拷贝把所有节点都复制一遍带来的性能损耗,Immutable 使用了结构共享(Structural Sharing),即当对象树中的一个节点发生变化时,只修改这个节点和受它影响的父节点,其它节点则进行共享:
二、ImmutableJS 的使用
ImmutableJS 提供了 7 种不可修改的数据类型:
- List
- Map
- Stack
- OrderedMap
- Set
- OrderedSet
- Record
1、ImmutableJS Map
类似于 key/value
的 object,在 ES6 也有原生 Map 对应:
const Map = Immutable.Map
// 1. Map 大小
const map1 = Map({ a: 1 })
map1.size
// => 1
// 2. 新增或取代 Map 元素
// set(key: K, value: V)
const map2 = map1.set('a', 7)
// => Map { "a": 7 }
// 3. 删除元素
// delete(key: K)
const map3 = map1.delete('a')
// => Map {}
// 4. 清除 Map 内容
const map4 = map1.clear()
// => Map {}
// 5. 更新 Map 元素
// update(updater: (value: Map<K, V>) => Map<K, V>)
// update(key: K, updater: (value: V) => V)
// update(key: K, notSetValue: V, updater: (value: V) => V)
const map5 = map1.update('a', () => 7)
// => Map { "a": 7 }
// 6. 合并 Map
const map6 = Map({ b: 3 })
map1.merge(map6)
// => Map { "a": 1, "b": 3 }
2、ImmutableJS List
有序且可以重复值,对应于一般的 Array:
const List = Immutable.List
// 1. 取得 List 长度
const arr1 = List([1, 2, 3])
arr1.size
// => 3
// 2. 新增或取代 List 元素内容
// set(index: number, value: T)
// 将 index 位置的元素替换
const arr2 = arr1.set(-1, 7)
// => [1, 2, 7]
const arr3 = arr1.set(4, 0)
// => [1, 2, 3, undefined, 0]
// 3. 删除 List 元素
// delete(index: number)
// 删除 index 位置的元素
const arr4 = arr1.delete(1)
// => [1, 3]
// 4. 插入元素到 List
// insert(index: number, value: T)
// 在 index 位置插入 value
const arr5 = arr1.insert(1, 2)
// => [1, 2, 2, 3]
// 5. 清空 List
// clear()
const arr6 = arr1.clear()
// => []
3、ImmutableJS Set
没有顺序且不能重复的列表:
const Set = Immutable.Set
// 1. 建立 Set
const set1 = Set([1, 2, 3])
// => Set { 1, 2, 3 }
// 2. 新增元素
const set2 = set1.add(1).add(5)
// => Set { 1, 2, 3, 5 }
// 由于 Set 为不能重复集合,故 1 只能出现一次
// 3. 删除元素
const set3 = set1.delete(3)
// => Set { 1, 2 }
// 4. 取联集
const set4 = Set([2, 3, 4, 5, 6])
set1.union(set4)
// => Set { 1, 2, 3, 4, 5, 6 }
// 5. 取交集
set1.intersect(set4)
// => Set { 2, 3 }
// 6. 取差集
set1.subtract(set4)
// => Set { 1 }
4、Immutable 类型转换
4-1、JS 转 Immutable
fromJS(value, [converter])
// value:要转变的数据
// converter:要做的操作
const obj = Immutable.fromJS({ a: '123', b: '234' })
4-2、Immutable 转 JS
immutableValue.toJS()
5、两个对象的比较
import { Map, is } from 'immutable'
const map1 = Map({ a: 1, b: 1, c: 1 })
const map2 = Map({ a: 1, b: 1, c: 1 })
map1 === map2 // false
Object.is(map1, map2) // false
is(map1, map2) // true
三、在 React 中的应用
使用 Immutable 在 React 中可以减少渲染次数,提升性能。
1、shouldComponentUpdate 优化
在使用 shouldComponentUpdate() 做性能优化时,Immutable 可以通过 is
方法完成对比,以替代较耗性能的深拷贝。
import { is } from 'immutable'
shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
const thisProps = this.props || {},
thisState = this.state || {}
if (
Object.keys(thisProps).length !== Object.keys(nextProps).length ||
Object.keys(thisState).length !== Object.keys(nextState).length
) {
return true
}
for (const key in nextProps) {
if (!is(thisProps[key], nextProps[key])) {
return true
}
}
for (const key in nextState) {
if (
thisState[key] !== nextState[key] &&
!is(thisState[key], nextState[key])
) {
return true
}
}
return false
}
2、setState 的优化
React 建议把 this.state
当作 Immutable 的,因此修改前需要做一个深拷贝,显得麻烦:
import '_' from 'lodash';
const Component = React.createClass({
getInitialState() {
return {
data: { times: 0 }
}
},
handleAdd() {
let data = _.cloneDeep(this.state.data);
data.times = data.times + 1;
this.setState({ data: data });
// 如果上面不做深拷贝,下面打印的结果会是已经加 1 后的值。
console.log(this.state.data.times);
}
}
使用 Immutable 后:
getInitialState() {
return {
data: Map({
times: 0
})
}
},
handleAdd() {
this.setState({
data: this.state.data.update('times', v => v + 1)
});
// 这时的 times 并不会改变
console.log(this.state.data.get('times'));
}
上面的 handleAdd
可以简写成:
handleAdd() {
this.setState(({data}) => ({
data: data.update('times', v => v + 1) })
)
}
3、Redux 中的数据优化
Redux 中也可以将数据进行 fromJS
处理:
import * as constants from './constants'
import { fromJS } from 'immutable'
const defaultState = fromJS({
// 将数据转化成 immutable 数据
home: true,
focused: false,
mouseIn: false,
list: [],
page: 1,
totalPage: 1
})
export default (state = defaultState, action) => {
switch (action.type) {
case constants.SEARCH_FOCUS:
return state.set('focused', true) // 更改 immutable 数据
case constants.CHANGE_HOME_ACTIVE:
return state.set('home', action.value)
case constants.SEARCH_BLUR:
return state.set('focused', false)
case constants.CHANGE_LIST:
// return state.set('list',action.data).set('totalPage',action.totalPage)
// merge 效率更高,执行一次改变多个数据
return state.merge({
list: action.data,
totalPage: action.totalPage
})
case constants.MOUSE_ENTER:
return state.set('mouseIn', true)
case constants.MOUSE_LEAVE:
return state.set('mouseIn', false)
case constants.CHANGE_PAGE:
return state.set('page', action.page)
default:
return state
}
}
四、总结
Immutable 是一旦创建就不能再被修改的数据,对 Immutable 对象进行修改时,会返回一个新的 Immutable 对象,以此来保证数据的不可变,它的作用是减少渲染次数,提升性能;
Immutable 使用了结构共享(即当对象树中的一个节点发生变化时,只修改这个节点和受它影响的父节点,其它节点则进行共享)来进行性能优化;
Object.assign 和 Map 在某些场景下可以替代 Immutable,但像 Redux 的数据优化,就无法替代 Immutable,因为 Redux 判断数据更新的条件是对象引用是否变化。