React项目小结系列:选择immutable.js的原因及实战浅谈

今天将主要说说自己之前选择immutable.js的原因以及具体项目中的实战技巧。

前提

在JavaScript的世界当中,对象是可变的,比如说修改一个浅拷贝的对象的属性的值的时候,原来对象也将发生同样的变化。比如说a={x:1};b=a;b.x=1,那么a.x也将为1。理由是因为对象浅拷贝只传递了引用,a和b是指向同一个对象,而js对象是可变的。

没有immutable.js的时候

在react官方推荐把this.state当作不可变。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
class Test extends React.Component{
constructor(props){
super(props);
this.state={
a:{ count:0 }
}
}
handleChange(){
//修改state,使用setState函数使得count变成5
}
}

我们声明如上的组件,如果我们此处要修改Test组件当中的state且使state不可变,显然如果直接在回调函数中使用this.setState({a:{count:this.state.count+5}})是不可以的。

那么如果我们使用深拷贝函数对state进行操作更改,当state变得臃肿之后,深拷贝的性能将是个巨大的问题。

同样的,如果我们使用redux。redux中的reducer是纯函数。如果新的state和旧的state的引用不发生变化,那么将返回旧的state。如下的例子中我们reducer的初始state为{count:0,num:0:}

1
2
3
4
case 'HANDLE_CHNANGE':
state.count = state.count+5;//change original object
return state;
default: ...

我们最终触发HANDLE_CHNANGE的action后得到的state仍为{count:0}。因为触发HANDLE_CHNANGE后state虽然改变了属性值(这是不符合纯函数特点的),但仍然指向旧的state,因此reducer通过浅比较仍然返回旧state。
那么如果我们要使得新的state最终被返回,必须返回一个新的对象。在上述例子中我们有很多方法,比如使用es6中的解构赋值{...state,count:state.count+5},使用Object.assign()方法。但是当我们的state深层嵌套的时候,问题来了那无论怎么样,我们又要使用类似深拷贝的方法,对嵌套内部的对象进行拷贝,性能受很大影响。

immutable.js概览

  • immutable.js提供了多种不可变数据结构:List、Map、Stack、OrderedMap、Set、OrderedSet和Record。
  • 齐全的API。基本的操作都可以满足,具体可以看官网文档
  • 相比于深拷贝,immutable.js是使用了structural sharing的技术。当我们对一个immutable对象进行操作的时候,immutable.js基于hash map trees和vector map tries,只拷贝当前修改的节点以及它的祖先节点,其他保持不变,不同的对象之间可以共享相同的部分。可以看下面这个例子:
1
2
3
4
5
6
7
let obj = {
a: 1,
b: {c:1}
}
let test1 = Immutable.fromJS(obj);
let test2 = map1.set('a', 2);
console.log(test1.c === test2.c); // true

从上面的代码,我们可以看test1和test2是两个不同的对象,但它们共享了c对象。structural sharing很大程度上提高了性能,节约时间成本。

本项目中我是怎么使用immutable.js的

有关涉及immutable.js的操作主要是在reducer和组件当中。

在reducer当中,我们要修改state,无论对于Map还是List等,我们只需要使用set()或者setIn()(多深层嵌套都可以使用)去修改我们的state而无需其他操作就会返回新的对象。举个例子:

1
2
3
case 'HANDLE_CHNANGE':
return state.set('count',state.get('count')+5);//返回一个新的对象
default: ...

另外一个地方使用immutable.js便是获取store当中数据的时候。本项目当中使用了react-redux库,因此把有关数据处理部分都在容器组件当中通过selector来处理,在此,我们处理的时候,一般可以使用get()或者getIn()来从Provider当中的store中获取我们要的props。举个例子:

1
2
3
4
const mapStateToProps = (state) => {
a:state.getIn(['c','a'),
b:state.getIn(['c','b'])
}

不过这边有一点要注意,当代码state.b是个immutable的Map或者List对象而不是一个基本类型的值的时候。我们可能对immutable的API不太熟悉,想把immutable对象转换成js的对象或数组使用(比如,我现在还是不知道immutable的List当中如何去使用类似js数组的map的方法)。这时我们有两个选择去把其转换成js对象,一个就是直接把immutable对象当作props传给展示组件,然后展示组件中再转换;另一种方案就是通过toJS()方法将其转换成普通的js对象再传递给展示组件。

相比较而言,我们应该选择前者,因为不可变数据对象在此有一个重要的作用:shouldComponentUpdate的性能优化。当接收immutable对象作为props的组件是一个使用shouldComponentUpdate进行性能优化的组件时,我们传递过去的是immutable对象的时候,shouldComponentUpdate只需要进行浅比较即可,当我们将immutable对象转换成js对象的时候,则shouldComponentUpdate中需要进行深比较,性能降低不少。

immutable化数据的作用

在React生态当中使用immutable数据可以使得状态可预测,可以预测和管理状态流动。同时也可以防止其他副作用,使得状态变化很纯粹清楚。immutable的数据在shouldComponentUpdate的性能优化当中也有很大作用。