大话immutable.js

为啥要用immutable.js呢。毫不夸张的说。有了immutable.js(当然也有其他实现库)。。才能将react的性能发挥到极致!要是各位看官用过一段时间的react,而没有用immutable那么本文非常适合你。

1,对于react的来说,如果父组建有多个子组建

想象一下这种场景,一个父组建下面一大堆子组建。然后呢,这个父组建re-render。是不是下面的子组建都得跟着re-render。可是很多子组建里面是冤枉的啊!!很多子组建的props 和 state 然而并没有改变啊!!虽然virtual dom 的diff 算法很快。。但是性能也不是这么浪费的啊!!

以下是父组件代码。。负责输入name 和 age 然后循环显示name 和 age

 1 export default class extends Component{
 2           constructor(props){
 3                super(props);
 4                this.state = {
 5           name:"",
 6           age :"",
 7           persons:[]
 8         }
 9    }
10          
11           render(){
12                const {name,age,persons} = this.state
13                return  (
14                      <div>
15                      <span>姓名:<input value={name} name="name" onChange={this._handleChange.bind(this)}></input>
16             <span>年龄:</span><input value={age} name="age" onChange={this._handleChange.bind(this)}></input>
17             <input type="button" onClick={this._handleClick.bind(this)} value="确认"></input>
18             {persons.map((person,index)=>(
19               <Person key={index} name={person.name} age={person.age}></Person>
20             ))}
21           </div>
22        )
23 
24 }
25       _handleChange(event){
26         this.setState({[event.target.name]:event.target.value})
27       }
28       _handleClick(){
29         const {name,age} = this.state
30         this.setState({
31           name:"",
32           age :"",
33                     persons:this.state.persons.concat([{name:name,age:age}])
34         })
35     
36       }
37 
38 }

以下是子组建代码单纯的显示name和age而已

 1 class Person  extends Component {
 2   componentWillReceiveProps(newProps){
 3     console.log(`我新的props的name是${newProps.name},age是${newProps.age}。我以前的props的name是${this.props.name},age是${this.props.age}是我要re-render了`);
 4   }
 5   render() {
 6     const {name,age} = this.props;
 7 
 8       return (
 9         <div>
10           <span>姓名:</span>
11           <span>{name}</span>
12           <span> age:</span>
13           <span>{age}</span>
14         </div>
15       )
16   }
17 }

这样看得出来了吧  每次添加人的时候就会导致子组件re-render了

2,PureRenderMixin

因为咱用的是es2015的 Component,所以已经不支持mixin了。。所以在这里我们用[Pure render decorator][5]代替PureRenderMixin,那么代码如下

 1 import pureRender from "pure-render-decorator"
 2 ...
 3 
 4 @pureRender
 5 class Person  extends Component {
 6   render() {
 7     console.log("我re-render了");
 8     const {name,age} = this.props;
 9 
10       return (
11         <div>
12           <span>姓名:</span>
13           <span>{name}</span>
14           <span> age:</span>
15           <span>{age}</span>
16         </div>
17       )
18   }
19 }

果然可以做到pure render。。在必须render 的时候才render

是es7的Decorators语法。上面这么写就和下面这么写一样

 1 class PersonOrigin  extends Component {
 2   render() {
 3     console.log("我re-render了");
 4     const {name,age} = this.props;
 5 
 6       return (
 7         <div>
 8           <span>姓名:</span>
 9           <span>{name}</span>
10           <span> age:</span>
11           <span>{age}</span>
12         </div>
13       )
14   }
15 }
16 const Person = pureRender(PersonOrigin)

pureRender其实就是一个函数,接受一个Component。把这个Component搞一搞,返回一个Component
看他pureRender的源代码就一目了然

1 function shouldComponentUpdate(nextProps, nextState) {
2   return shallowCompare(this, nextProps, nextState);
3 }
4 
5 function pureRende(component) {
6   component.prototype.shouldComponentUpdate = shouldComponentUpdate;
7    return component;
8 }
9 module.exports = pureRender;

pureRender很简单,就是把传进来的component的shouldComponentUpdate给重写掉了,原来的shouldComponentUpdate,无论怎样都是return ture,现在不了,我要用shallowCompare比一比,shallowCompare代码及其简单,如下

1 function shallowCompare(instance, nextProps, nextState) {
2   return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
3 }

一目了然。分别拿现在props&state和要传进来的props&state,用shallowEqual比一比,要是props&state都一样的话,就return false,是不是感觉很完美?不。。这才刚刚开始,问题就出在shallowEqual上了

很多时候,父组件向子组件传props的时候,可能会传一个复杂类型,比如我们改下。

 1 render() {
 2     const {name,age,persons} = this.state
 3     return (
 4       <div>
 5 ...省略.....
 6         {persons.map((person,index)=>(
 7           <Person key={index} detail={person}></Person>
 8         ))}
 9       </div>
10     )
11   }

person是一个复杂类型。。这就埋下了隐患,,在演示隐患前,我们先说说shallowEqual,是个什么东西,shallowEqual其实只比较props的第一层子属性是不是相同,就像上述代码,props 是如下
{
detail:{
name:"123",
age:"123"}
}

他只会比较props.detail ===nextProps.detail
那么问题来了,上代码
如果我想修改detail的时候考虑两种情况

 

情况一,我修改detail的内容,而不改detail的引用

这样就会引起一个bug,比如我修改detail.name,因为detail的引用没有改,所以
props.detail ===nextProps.detail 还是为true。。
所以我们为了安全起见必须修改detail的引用,(redux的reducer就是这么做的)

 

情况二,我修改detail的引用

这种虽然没有bug,但是容易误杀,比如如果我新旧两个detail的内容是一样的,岂不是还要,render。。所以还是不完美,,你可能会说用 深比较就好了,,但是 深比较及其消耗性能,要用递归保证每个子元素一样.

 

有人说 Immutable 可以给 React 应用带来数十倍的提升,也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,因为同期 React 太火,它的光芒被掩盖了。这些至少说明 Immutable 是很有价值的,下面我们来一探究竟。

JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。如 foo={a: 1}; bar=foo; bar.a=2 你会发现此时 foo.a 也被改成了 2。虽然这样做可以节约内存,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。

 

什么是 Immutable Data

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:

 1 // 原来的写法
 2 let foo = {a: {b: 1}};
 3 let bar = foo;
 4 bar.a.b = 2;
 5 console.log(foo.a.b);  // 打印 2
 6 console.log(foo === bar);  //  打印 true
 7 
 8 // 使用 immutable.js 后
 9 import Immutable from 'immutable';
10 foo = Immutable.fromJS({a: {b: 1}});
11 bar = foo.setIn(['a', 'b'], 2);   // 使用 setIn 赋值
12 console.log(foo.getIn(['a', 'b']));  // 使用 getIn 取值,打印 1
13 console.log(foo === bar);  //  打印 false
14 
15 // 使用  seamless-immutable.js 后
16 import SImmutable from 'seamless-immutable';
17 foo = SImmutable({a: {b: 1}})
18 bar = foo.merge({a: { b: 2}})   // 使用 merge 赋值
19 console.log(foo.a.b);  // 像原生 Object 一样取值,打印 1
20 console.log(foo === bar);  //  打印 false
1 function touchAndLog(touchFn) {
2   let data = { key: 'value' };
3   touchFn(data);
4   console.log(data.key); // 猜猜会打印什么?
5 }

在不查看 touchFn 的代码的情况下,因为不确定它对 data 做了什么,你是不可能知道会打印什么(这不是废话吗)。但如果 data 是 Immutable 的呢,你可以很肯定的知道打印的是 value

 

1 import { Map} from 'immutable';
2 let a = Map({
3   select: 'users',
4   filter: Map({ name: 'Cam' })
5 })
6 let b = a.set('select', 'people');
7 
8 a === b; // false
9 a.get('filter') === b.get('filter'); // true

上面 a 和 b 共享了没有变化的 filter 节点。

Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 map.get('key') 而不是 map.keyarray.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。

两个 immutable 对象可以使用 === 来比较,这样是直接比较内存地址,性能最好。但即使两个对象的值是一样的,也会返回 false

1 let map1 = Immutable.Map({a:1, b:1, c:1});
2 let map2 = Immutable.Map({a:1, b:1, c:1});
3 map1 === map2;             // false
Immutable.is(map1, map2);  // true

Immutable.is 比较的是两个对象的 hashCode 或 valueOf(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。

后面会使用 Immutable.is 来减少 React 重复渲染,提高性能。

由于 Immutable 数据一般嵌套非常深,为了便于访问深层数据,Cursor 提供了可以直接访问这个深层数据的引用。

 1 import Immutable from 'immutable';
 2 import Cursor from 'immutable/contrib/cursor';
 3 
 4 let data = Immutable.fromJS({ a: { b: { c: 1 } } });
 5 // 让 cursor 指向 { c: 1 }
 6 let cursor = Cursor.from(data, ['a', 'b'], newData => {
 7   // 当 cursor 或其子 cursor 执行 update 时调用
 8   console.log(newData);
 9 });
10 
11 cursor.get('c'); // 1
12 cursor = cursor.update('c', x => x + 1);
13 cursor.get('c'); // 2

 

setState 的一个技巧

React 建议把 this.state 当作 Immutable 的,因此修改前需要做一个 deepCopy,显得麻烦:

 1 import '_' from 'lodash';
 2 
 3 const Component = React.createClass({
 4   getInitialState() {
 5     return {
 6       data: { times: 0 }
 7     }
 8   },
 9   handleAdd() {
10     let data = _.cloneDeep(this.state.data);
11     data.times = data.times + 1;
12     this.setState({ data: data });
13     // 如果上面不做 cloneDeep,下面打印的结果会是已经加 1 后的值。
14     console.log(this.state.data.times);
15   }
16 }

使用 Immutable 后:

 1 getInitialState() {
 2     return {
 3       data: Map({ times: 0 })
 4     }
 5   },
 6   handleAdd() {
 7     this.setState({ data: this.state.data.update('times', v => v + 1) });
 8     // 这时的 times 并不会改变
 9     console.log(this.state.data.get('times'));
10   }

上面的 handleAdd 可以简写成:

1 handleAdd() {
2     this.setState(({data}) => ({
3       data: data.update('times', v => v + 1) })
4     });
5   }

与 Flux 搭配使用

由于 Flux 并没有限定 Store 中数据的类型,使用 Immutable 非常简单。

现在是实现一个类似带有添加和撤销功能的 Store:

 1 import { Map, OrderedMap } from 'immutable';
 2 let todos = OrderedMap();
 3 let history = [];  // 普通数组,存放每次操作后产生的数据
 4 
 5 let TodoStore = createStore({
 6   getAll() { return todos; }
 7 });
 8 
 9 Dispatcher.register(action => {
10   if (action.actionType === 'create') {
11     let id = createGUID();
12     history.push(todos);  // 记录当前操作前的数据,便于撤销
13     todos = todos.set(id, Map({
14       id: id,
15       complete: false,
16       text: action.text.trim()
17     }));
18     TodoStore.emitChange();
19   } else if (action.actionType === 'undo') {
20     // 这里是撤销功能实现,
21     // 只需从 history 数组中取前一次 todos 即可
22     if (history.length > 0) {
23       todos = history.pop();
24     }
25     TodoStore.emitChange();
26   }
27 });

Mutable 对象
在 JavaScript 中,对象是引用类型的数据,其优点在于频繁的修改对象时都是在原对象的基础上修改,并不需要重新创建,这样可以有效的利用内存,不会造成内存空间的浪费,对象的这种特性可以称之为 Mutable,中文的字面意思是「可变」。
对于 Mutable 的对象,其灵活多变的优点有时可能会成为其缺点,越是灵活多变的数据越是不好控制,对于一个复杂结构的对象来说,一不小心就在某个不经意间修改了数据,假如该对象又在多个作用域中用到,此时很难预见到数据是否改变以及何时改变的。

针对这种问题,常规的解决办法可以通过将对象进行深拷贝的形式复制出一个新的对象,再在新对象上做修改的操作,这样能确保数据的可控性,但是频繁的复制会造成内存空间的大量浪费。

1
2
3
4
5
6
var obj = { /* 一个复杂结构的对象 */ };
// copy 出一个新的 obj2
// 但是 copy 操作会浪费内存空间
var obj2 = deepClone(obj);
doSomething(obj2);
// 上面的函数之行完后,无论 obj2 是否变化,obj 肯定还是原来那个 obj
Mutable 和 Immutable 的性能对比
对于 Mutable 的对象的低效率操作主要体现在复制和比较上,而 Immutable 对象就是解决了这两大低效的痛点。
普通的 Mutable 对象的深拷贝操作会将一整份数据都复制一遍,而 Immutable 对象在修改数据时并不会复制一整份数据,而是将变化的节点与未变化的节点的父子关系转移到一个新节点上,类似于链表的结构。从 “复制” 的角度来看,做到了最小化的复制,未变化的部分都是共享的,Mutable 在复制的时候是 “全量”,而 Immutable 复制的是 “增量”,对于内存空间的使用率的比较高低立判。
并且基于每次修改一个 Immutable 对象都会创建一个新的 Immutable 对象的这种特性可以将数据的修改状态保存成一组快照,这也是挺方便的。
再来说说比较操作。对于 Mutable 的对象,如果要比较两个对象是否相等,必须遍历对象的每个节点进行比较,对于结构复杂的对象来说,其效率肯定高不到哪去。对于 Immutable 对象,immutable.js 提供了直接判断两个 Immutable 对象的「值」是否相等的 API。
 
var map1 = Immutable.Map({a:1, b:1, c:1});
var map2 = Immutable.Map({a:1, b:1, c:1});
assert(map1 !== map2); // 不同的 Immutable 实例,此时比较的是引用地址
assert(Immutable.is(map1, map2)); // map1 和 map2 的值相等,比较的是值
assert(map1.equals(map2)); // 与 Immutable.is 的作用一样
 
var mutableObj = {};
// 写入数据
mutableObj.foo = 'bar';
// 读取数据
console.log(mutableObj.foo);
 
而 Immutable 对象需要通过 set 和 get 来对数据进行「读」和「写」。
var immutableObj1 = Immutable.Map();
// 写入数据
var immutableObj2 = immutableObj1.set('foo', 'bar');
// 读取数据
console.log(immutableObj2.get('foo')); // => 'bar'
 
对于层级比较深的数据,immutable.js 提供的访问接口很方便。
var immutableObj1 = Immutable.fromJS({
 a: {
  b: 'c'
 },
 d: [1, 2, 3]
});
// 读取深层级的数据
console.log(immutableObj1.getIn(['a', 'b'])); // => 'c'
console.log(immutableObj1.getIn(['d', 1])); // => 2
// 修改深层级的数据
var immutableObj2 = immutableObj1.setIn(['a', 'b'], 'd');
console.log(immutableObj2.getIn(['a', 'b'])); // => 'd'
 
好了,今天就这么多,慢慢消化,后期我回深入讲解immutable的api。
posted @ 2016-10-26 22:09  coder_231  阅读(8503)  评论(0编辑  收藏  举报