Skip to main content

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

上面 ab 共享了没有变化的 filter 节点。

3、Immutable 的缺点

  • 增加了资源文件大小

  • 容易与原生对象混淆

Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作不同,例如要用 map.get('key') 而不是 map.keyarray.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。当使用外部库时,一般需要使用原生对象,也很容易忘记转换。

可以通过以下办法来解决:

  1. 使用 Flow 或 TypeScript 等静态类型检查工具;
  2. 约定变量命名规则:如所有 Immutable 类型对象以 $$ 开头。
  3. 使用 Immutable.fromJS 而不是 Immutable.MapImmutable.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 判断数据更新的条件是对象引用是否变化。