有人说 Immutable 可以给 React 应用带来数十倍的提升,也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,因为同期 React 太火,它的光芒被掩盖了。这些至少说明 Immutable 是很有价值的,下面我们来一探究竟。
1、什么是Immutable?
Immutable是一旦创建,就不能被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象。Immutable实现的原理是Persistent Data Structure(持久化数据结构),也就是是永久数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免deepCopy把所有节点都复制一遍带来的性能损耗,Immutable使用了Structural Sharing(结构共享),即如果对象树结点发生变化,只修改这个结点和受它影响的父节点,其他结点进行共享。
初识:
让我们看下面一段代码:
function keyLog(touchFn) { let data = { key: ‘value‘ }; fun(data); console.log(data.key); // 猜猜会打印什么? }
不查看fun方法,不知道它对data做了什么,无法确认会打印什么。但如果data是Immutable,你可以确定打印的就是value:
function keyLog(touchFn) { let data = Immutable.Map({ key: ‘value‘ }); fun(data); console.log(data.get(‘key‘)); // value }
JavaScript中的Object与Array等使用的是引用赋值,如果新的对象简单的引用了原始对象,改变新的对象也将影响旧的。
foo = {a:1}; bar = foo; bar.a = 2; foo.a // 2
虽然这样可以节约内存,但当应用复杂后,造成了状态不可控,是很大的隐患,节约内存的优点变得得不偿失。
Immutable则不一样,相应的:
foo = Immutable.Map({ a: 1}); bar = foo.set(‘a‘, 2); foo.get(‘a‘) // 1
简洁:
在redux中,它的最优做法是每个reducer都返回一个新的对象(数组),所以我们常常会看到这样的代码:
// reducer ... return [ ...oldArr.slice(0,3), newValue, ...oldArr.slice(4) ];
为了返回新的对象(数组),不得不有上面奇怪的样子,而在使用更深的数据结构时会变的更棘手。
让我们看看Immutable的做法:
// reducer ... return oldArr.set(4, newValue);
是不是很简洁?
关于"===":
众所周知,对于Object与Array的===比较,是对引用地址的比较,而不是“值比较”,如:
{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false [1, 2, [3, 4]] === [1, 2, [3, 4]]; // false
对于上面只能采用deepCopy、deepCompare来比较,不仅麻烦而且耗性能。
我们感受一下Immutbale的做法:
map1 = Immutable.Map({a:1, b:2, c:3}); map2 = Immutable.Map({a:1, b:2, c:3}); Immutable.is(map1, map2); // true 比较值
map1 === map2; // false 比较地址
// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]); List1 = Immutable.fromJS([1, 2, [3, 4]]); List2 = Immutable.fromJS([1, 2, [3, 4]]); Immutable.is(List1, List2); // true
似乎有阵清风吹过。
Immutable.is
比较的是两个对象的 hashCode
或 valueOf
(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode
相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。
Immutable使用了Structure Sharing会尽量复用内存,甚至以前使用的对象也可以再次被复用,引用的对象会被垃圾回收。
import { Map} from ‘immutable‘; let a = Map({ select: ‘users‘, filter: Map({ name: ‘Cam‘ }) }) let b = a.set(‘select‘, ‘people‘); a === b; // false a.get(‘filter‘) === b.get(‘filter‘); // true
上面 a 和 b 共享了没有变化的 filter
节点。
并发安全:
传统的并发非常难做,因为要处理各种数据不一致问题,因此『聪明人』发明了各种锁来解决。但使用了 Immutable 之后,数据天生是不可变的,并发锁就不需要了。
然而现在并没什么卵用,因为 JavaScript 还是单线程运行的啊。但未来可能会加入,提前解决未来的问题不也挺好吗?
函数式编程:
Immutable本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。
像 ClojureScript,Elm 等函数式编程语言中的数据类型天生都是 Immutable 的,这也是为什么 ClojureScript 基于 React 的框架 --- Om 性能比 React 还要好的原因。
2、在react中使用Immutable
熟悉React的都知道,React做性能优化时有个大招,就是使用shouldComponentUpdate(),但它默认返回true,即始终会执行render()方法,后面做Virtual DOM比较,并得出是都需要做真是DOM更新,这里往往会带来很多务必要的渲染成为性能瓶颈。
在使用原生属性时,为了得出shouldComponetUpdate正确的true or false,不得不用deepCopy、deepCompare来算出答案,但 deepCopy 和 deepCompare 一般都是非常耗性能的。
而在有了Immutable之后,Immutable 则提供了简洁高效的判断数据是否变化的方法,来减少 React 重复渲染,提高性能,只需 ===
和 is
比较就能知道是否需要执行 render()
,而这个操作几乎 0 成本,所以可以极大提高性能。修改后的 shouldComponentUpdate
是这样的:
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; }
使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:
setState 的一个技巧
React 建议把 this.state
当作 Immutable 的,因为修改前需要做一个 deepCopy,显得麻烦:
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 }); // 如果上面不做 cloneDeep,下面打印的结果会是已经加 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中使用Immutable
目标:将state
-> Immutable化。
关键的库:gajus/redux-immutable
将原来 Redux提供的combineReducers改由上面的库提供:
// rootReduers.js // import { combineReducers } from ‘redux‘; // 旧的方法 import { combineReducers } from ‘redux-immutable‘; // 新的方法 import prop1 from ‘./prop1‘; import prop2 from ‘./prop2‘; import prop3 from ‘./prop3‘; const rootReducer = combineReducers({ prop1, prop2, prop3, }); // store.js // 创建store的方法和常规一样 import { createStore } from ‘redux‘; import rootReducer from ‘./reducers‘; const store = createStore(rootReducer); export default store;
通过新的combineReducers
将把store对象转化成Immutable,在container中使用时也会略有不同(但这正是我们想要的):
const mapStateToProps = (state) => ({ prop1: state.get(‘prop1‘), prop2: state.get(‘prop2‘), prop3: state.get(‘prop3‘), next: state.get(‘next‘), }); export default connect(mapStateToProps)(App);
4、总结
Immutable 可以给应用带来极大的性能提升,但是否使用还要看项目情况。由于侵入性较强,新项目引入比较容易,老项目迁移需要评估迁移。对于一些提供给外部使用的公共组件,最好不要把 Immutable 对象直接暴露在对外接口中。
如果 JS 原生 Immutable 类型会不会太美,被称为 React API 终结者的 Sebastian Markb?ge 有一个这样的提案,能否通过现在还不确定。不过可以肯定的是 Immutable 会被越来越多的项目使用。