大话immutable.js

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

1,对于react的来讲,若是父组建有多个子组建

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

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

 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而已api

 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语法。上面这么写就和下面这么写同样less

 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的源代码就一目了然dom

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。
相关文章
相关标签/搜索