痛点
在我们的印象中,React
好像就意味着组件化、高性能,我们永远只需要关心数据整体,两次数据之间的 UI
如何变化,则完全交给 React
的 Diff
算法去做。以至于我们很随意的去操纵数据,shuouldComponentUpdate
也懒得去写,反正不写也能正确渲染。但随着应用体积越来越大,会发现页面好像有点变慢了,特别是组件嵌套比较多,数据结构变复杂的情况下,随便改变一个表单,或者对列表做一个筛选都要耗时 100ms
左右,这个时候我们就需要优化了!当然如果如果没有遇到性能瓶颈,完全不用担心,过早优化是邪恶的。这里我们提供一个很简单的方案来让 React
应用性能发挥到极致,为了照顾基础比较弱的读者,我们先回顾一下 JavaScript
变量类型和React
渲染机制,如果你是老鸟可以直接跳过。
变量类型
JavaScript的变量类型有两类:
- 基本类型:6 种基本数据类型,
Undefined
、Null
、Boolean
、Number
、String
、Symbol
- 引用类型:统称为
Object
类型,细分为:Object
类型、Array
类型、Date
类型、RegExp
类型、Function
类型等。
举个例子:
1
2
3
4
5
6
7
8
9
10
|
// 基本类型
let a =
'test';
let b = a;
b =
'test1';
console.log(a);
// test
// 引用类型
let p1 = {
name:
'neo' };
let p2 = p1;
p2.name =
'dave';
console.log(p1.name);
// dave
|
在引用类型里,声明一个 p1
的对象,把 p1
赋值给 p2
,修改 p2
的 name
属性,结果 p1
的属性也修改了,也就是说 p1
与 p2
其实是公用一个引用,虽然这样做可以节约内存,但当应用复杂后,就需要很小心的操作数据了,因为一不注意修改一个变量的值可能就影响到了另外一个变量。如果我们想要让他们不影响,就需要拷贝出一份一模一样的数据,拷贝又分浅拷贝与深拷贝,浅拷贝只会拷贝第一层的数据,深拷贝则会递归所有层级都拷贝一份,比较消耗性能。
React
在 React
中,每次 setState
, Virtual DOM
算法会计算出前后两次虚拟 DOM
对象的区别,再去修改真实需要修改的 DOM
。由于 js
计算速度很快,而操作真实 DOM
是昂贵的消耗,Virtual DOM
算法避免了没必要的真实 DOM
操作,所以 React
性能很好。但随着应用复杂度的提升, DOM
树越来越复杂,大量的对比操作也会影响性能。比如一个 Table
组件,修改其中一行 Tr
组件的某一个字段, setState
后,其他所有行 Tr
组件也都会执行一次 render
函数,这其实是不必要的。我们可以通过 shuouldComponentUpdate
函数决定是否更新组件。大部分时候我们是可以知道哪些组件是不会变的,根本就没必要去计算那一部分虚拟 DOM
。
PureComponent
React15.3
中新加了一个类,前身是 PureRenderMixin
,和 Component
基本一样,只不过会在 render
之前帮组件自动执行一次shallowEqual(浅比较),来决定是否更新组件,浅比较类似于浅复制,只会比较第一层。使用 PureComponent
相当于省去了写 shouldComponentUpdate
函数,当组件更新时,如果组件的 props
和 state
都没发生改变, render
方法就不会触发。如果 props
和 state
虽然值没变,但引用变了,就会造成虚拟 DOM
计算的浪费,如果值改了,但引用没改,又会造成不渲染,所以需要很小心的操作数据。
Immutable.js
Immutable.js是 Facebook
在 2014
年出的持久性数据结构的库,数据一旦创建,就不能再被更改,任何修改或添加删除操作都会返回一个新的 Immutable
对象。可以让我们更容易的去处理缓存、回退、数据变化检测等问题,简化开发。
1
2
3
4
5
6
7
|
import {
Map }
from
"immutable";
const map1 =
Map({
a: {
aa:
1 },
b:
2,
c:
3 });
const map2 = map1.set(
'b',
50);
map1 !== map2;
// true
map1.get(
'b');
// 2
map2.get(
'b');
// 50
map1.get(
'a') === map2.get(
'a');
// true
|
ImmutableJS
提供了7种不可修改的数据类型:List
、 Map
、 Stack
、 OrderedMap
、 Set
、 OrderedSet
、 Record
。其中比较常用的有 List
、 Map
和 Set
:
- Map:无序的
key
、value
集合,对应js
的Object
类型 - List:有序且可以重复的集合,对应
js
的Array
类型 - Set:没有顺序且不能重复的列表
这些类型提供了大量的方法去更新、删除、添加数据,极大的方便了我们操纵数据。除此之外,还提供了原生类型与 ImmutableJS
类型判断与转换方法:
1
2
3
4
5
6
7
8
|
import { fromJS, isImmutable }
from
"immutable";
const obj = fromJS({
a:
'test',
b: [
1,
2,
4]
});
// 支持混合类型
isImmutable(obj);
// true
obj.size();
// 2
const obj1 = obj.toJS();
// 转换成原生 `js` 类型
|
ImmutableJS
最大的两个特性就是: immutable data structures
(持久性数据结构)与 structural sharing
(结构共享),持久性数据结构保证数据一旦创建就不能修改,使用旧数据创建新数据时,旧数据也不会改变,不会像原生 js
那样新数据的操作会影响旧数据。而结构共享是指没有改变的数据共用一个引用,这样既减少了深拷贝的性能消耗,也减少了内存。比如下图:
我需要改变红色节点的值,生成的新值改变了红色节点到根节点路径之间的所有节点,也就是所有青色节点的值,旧值没有任何改变,其他使用它的地方并不会受影响,而超过一大半的蓝色节点还是和旧值共享的。在 ImmutableJS
内部,构造了一种特殊的数据结构,把原生的值结合一系列的私有属性,创建成ImmutableJS
类型,每次改变值,先会通过私有属性的辅助检测,然后改变对应的需要改变的私有属性和真实值,最后生成一个新的值,中间会有很多的优化,所以性能会很高。
案例
首先我们看看只使用 React
的情况下,应用性能为什么会被浪费,代码地址:下载,这个案例使用 create-react-app
,检测工具使用 chrome
插件:React Perf。执行
1
2
|
yarn
yarn start
|
可以打开页面,开始记录,然后随便对以一列数据进行修改,结束记录,可以看到我们仅修改了一行数据,但在 Print Wasted
那一项里,渲染 Tr
组件浪费了5次:
无论是添加,删除操作,都会浪费 n-1
次 render
,因为 App
组件的整个 state
改变了,所有的组件都会重新渲染一次,最后对比出需要真实 DOM
的操作。如果把 Table
组件和 Tr
继承的 Component
改成 PureComponent
,那么, Tr
组件每次更新都会进行一次 shallowEqual
比较,修改操作就没有了浪费,可是添加和删除操作却无效了,添加的操作是:
1
2
3
4
5
6
7
|
add =
() => {
const { data } =
this.state;
data.push(dataGenerate())
this.setState({
data
})
}
|
data.push
并没有改变 data
的引用,所以 PureComponent
的浅比较直接返回了 true
,不去 render
了。这并不是我们想要的,所以如果使用 Component
必定带来性能浪费,使用 PureComponent
又需要小心的操纵数据,不然一不小心就出错了。
这个时候, ImmutableJS
就可以显示出它的威力了,因为它只会改变需要改变的那条路径,我们看看修改后的例子:代码地址:下载,执行上面例子同样的操作,可以看到:
添加,删除,修改操作,没有一次浪费。可以看出, PureComponent
与 ImmutableJS
简直是天生一对啊,一个只有 props
或 state
修改了才会 re-render
,一个当且仅当修改需要修改的路径,如果结合 redux
,那就更加完美了。因为 redux
的 reducer
每次返回的必须是一个新的引用,有时候我们必须使用 clone
或者 assign
等操作来确保返回新引用,如果使用 ImmutanleJS
根本就不需要 lodash
等函数库了,比如我使用redux + immutable + react-router + express
写了一个稍微复杂点的例子:下载,可以看到 store
的状态是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{
loading:
false,
tableData: [{
"name":
"gyu3w0oa5zggkanciclhm2t9",
"age":
64,
"height":
121,
"width":
71,
"hobby": {
"movie": {
"name":
"zrah6zrvm9e512qt4typhkt9",
"director":
"t1c69z1vd4em1lh747dp9zfr"
}
}
}],
totle:
0
}
|
如果我需要快速修改 width
的值为90,可以使用以下几种方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// payload = { name: 'gyu3w0oa5zggkanciclhm2t9', width: 90 }
// 1. 使用深拷贝
updateWidth(state, payload) {
const newState = deepClone(state);
return newState.tableData.map(
item => {
if (tem.name === payload.name) {
item.width = payload.width;
}
return item;
});
}
// 2. 使用Object.assign
updateWidth(state, payload) {
return
Object.assign({}, state, {
tableData: state.state.map(
item => {
if (item.name === payload.name) {
return
Object.assign({}, item, {
width: payload.width });
}
return item;
})
})
}
// 3. 使用ImmutableJS
updateWidth(state, payload) {
return state.update(
'tableData', list => list.update(
list.findIndex(
(item) => item.get(
'name') === payload.name),
item => item.set(
'width', payload.width)));
}
|
使用深拷贝是一个昂贵的操作,而且引用都改变了,必然造成 re-render
, 而 Object.assign
会浅复制第一层,虽然不会造成 re-render
,但浅复制把其他的属性也都复制了一次,在这里也是很没有必要的,只有使用 ImmutableJS
完美的完成了修改,并且代码也最少。
优势与不足
可以看出, ImmutableJS
结合 PureComponent
可以很大程度的减少应用 re-render
的次数,可以大量的提高性能,并且提供了大量的类似原生 JS
的方法,还有 Lazy Operation
的特性,完全函数式编程,很容易实现 Redo/Undo
历史回顾。但还是有一些不足的地方:
- 获取组件属性必须用
get
或getIn
操作(除了RecordR类型),这样和原生的.
操作比起来就麻烦多了,如果组件之前已经写好了,还需要大量的修改。 ImmutableJS
库体积比较大,大概56k,开启gzip
压缩后16k。- 学习成本。
- 难以调试,在
redux-logger
里面需要在stateTransformer
配置里执行state.toJS()
。
最佳实践
其实,重要的是编程者需要有性能优化的意识,熟悉 js
引用类型的特性,了解事情的本质比会使用某个框架或库更加重要。用其他的方法也是完全可以达到 ImmutableJS
的效果,比如添加数据可以使用解构操作符的方式:
1
2
3
4
5
6
|
add =
() => {
const { data } =
this.state;
this.setState({
data: [...data, dataGenerate()]
})
}
|
只不过如果数据嵌套比较深,写起来还是比较麻烦。以下有一些小技巧:
- 还有两个轻量库可以实现不可变数据结构:seamless-immutable或者immutability-helper,只不过原理完全不一样,效率也没那么高。
- 避免大量使用
toJS
操作,这样会浪费性能。 - 不要将简单的
JavaScript
对象与Immutable.JS
混合 - 结合
redux
的时候,要使用import { combineReducers } from 'redux-immutablejs';
,因为redux
的combineReducers
期望state
是一个纯净的js
对象。 - 尽量将
state
设计成扁平状的。 - 展示组件不要使用
Immutable
数据结构。 - 不要在
render
函数里一个PureComponent
组件的props
使用bind(this)
或者style={ { width: '100px' } }
,因为浅比较一定会对比不通过。
参考链接
- Immutable.js, persistent data structures and structural sharing
- immutable.js is much faster than native javascript
- Immutable 详解及 React 中实践