React在减少重复渲染方面确实是有一套独特的处理办法,那就是虚拟DOM,但显然在首次渲染的时候React绝无可能超越原生的速度,或者一定能将其它的框架比下去。尤其是在优化前的React,每次数据变动都会执行render,大大影响了性能,特别是在移动端。
React 默认的渲染行为
初始化渲染
在初始化渲染时,我们需要渲染整个应用
(绿色 = 已渲染节点)
提出改变
我们想更新一部分数据。这些改变只和一个叶子节点相关(绿色的)
理想更新
我们只想渲染通向叶子节点的关键路径上的这几个节点(绿色的)
默认行为
如果你不告诉 React 别这样做,它便会如此
(橘黄色 = 浪费的渲染)
从上图可以看见,组件除了必要渲染的三个节点外,还渲染了其他不必要渲染的节点,这对性能是一个很大的浪费。如果对于复杂的页面,这将导致页面的整体体验效果非常差。因此要提高组件的性能,就应该想尽一切方法减少不必要的渲染。
React的生命周期
React的生命周期如下,还没熟悉的同学可以去熟悉一下。
shouldComponentUpdate
因为其中的 shouldComponentUpdate
是优化的关键。React的重复渲染优化的核心其实就是在shouldComponentUpdate里面做数据比较。在优化之前,shouldComponentUpdate
是默认返回true的,这导致任何时候触发任何的数据变化都会使component重新渲染。这必然会导致资源的浪费和性能的低下——你可能会感觉比较原生的响应更慢。
React性能优化的关键在于shouldComponentUpdate
,
在上面的示例中,因为 C2 的 shouldComponentUpdate
返回 false,React 就不需要生成新的虚拟 DOM,也就不需要更新 DOM,注意 React 甚至不需要调用 C4 和 C5 的 shouldComponentUpdate
。
C1 和 C3 的 shouldComponentUpdate
返回 true,所以 React 需要向下到叶子节点检查它们,C6 返回 true,因为虚拟 DOM 不相等,需要更新 DOM。最后感兴趣的是 C8,对于这个节点,React 需要计算虚拟 DOM,但是因为它和旧的相等,所以不需要更新 DOM。
React.PureComponent
在传入组件的props和state只有一层时,我们可以直接使用 React.PureComponent,它会自动帮我们进行浅比较(shallow-compare),从而控制shouldComponentUpdate的返回值。
但是,当传入props或state不止一层,或者未array和object时,浅比较(shallow-compare
)就失效了。当然我们也可以在 shouldComponentUpdate()
中使用使用 deepCopy
和 deepCompare
来避免无必要的 render()
,但 deepCopy
和 deepCompare
一般都是非常耗性能的。这个时候我们就需要 Immutable
。
Immutable
JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。如
foo={a: 1};
bar=foo;
bar.a=2
你会发现此时 foo.a 也被改成了 2。虽然这样做可以节约内存,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用 shallowCopy
(浅拷贝)或 deepCopy
(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。
而Immutable 可以很好地解决这些问题。
什么是Immutable Data
Immutable Data
就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure
(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing
(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
可以看看下面这个经典的动画:
immutable.js
Immutable.js本质上是一个JavaScript的持久化数据结构的库 ,但是由于同期的React太火,并且和React在性能优化方面天衣无缝的配合,导致大家常常把它们两者绑定在一起。
Immutable.js是Facebook 工程师 Lee Byron 花费 3 年时间打造,但没有被默认放到 React 工具集里(React 提供了简化的 Helper)。它内部实现了一套完整的 Persistent Data Structure
,且数据结构和方法非常丰富(完全不像JS出身的好不好)。像 Collection、List、Map、Set、Record、Seq。有非常全面的map、filter、groupBy、reduce、find函数式操作方法。同时 API 也尽量与 Object 或 Array 类似。 Immutable.js 压缩后下载有 16K。
其中有 3 种最重要的数据结构说明一下:(Java 程序员应该最熟悉了)
Map
:键值对集合,对应于 Object,ES6 也有专门的 Map 对象List
:有序可重复的列表,对应于 ArraySet
:无序且不可重复的列表
简单示例
import { Map } from "immutable";
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
seamless-immutable
seamless-immutable是另一套持久化数据结构的库,它并没有实现完整的 Persistent Data Structure
,而是使用 Object.defineProperty
(因此只能在 IE9 及以上使用)扩展了 JavaScript 的 Array 和 Object 对象来实现,只支持 Array 和 Object 两种数据类型,API 基于与 Array 和 Object ,因此许多不用改变自己的使用习惯,对代码的入侵非常小。同时,它的代码库也非常小,压缩后下载只有 2K。
简单示例
// 使用 seamless-immutable.js 后
import Immutable from 'seamless-immutable';
var array = Immutable(["totally", "immutable", {hammer: "Can’t Touch This"}]);
array[1] = "I'm going to mutate you!"
array[1] // "immutable"
array[2].hammer = "hm, surely I can mutate this nested object..."
array[2].hammer // "Can’t Touch This"
for (var index in array) { console.log(array[index]); }
// "totally"
// "immutable"
// { hammer: 'Can’t Touch This' }
JSON.stringify(array) // '["totally","immutable",{"hammer":"Can’t Touch This"}]'
seamless-immutable的实现依赖于ECMAScript 5 的一些特性,如Object.defineProperty 和 Object.freeze,因此会在浏览器兼容性方面有所欠缺
不过这不是问题啦,可以使用 polyfill es-shims/es5-shim 来解决。
对比
虽然 Immutable.js
尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。
Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 map.get('key')
而不是 map.key
,array.get(0)
而不是 array[0]
。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。
当使用外部库的时候,一般需要使用原生对象,也很容易忘记转换。
当然也有一些办法来避免类似问题发生:
使用 Flow 或 TypeScript 这类有静态类型检查的工具
约定变量命名规则:如所有 Immutable 类型对象以 开头。
使用
Immutable.fromJS
而不是Immutable.Map
或Immutable.List
来创建对象,这样可以避免 Immutable 和原生对象间的混用。
但是还有一个致命的问题是,对现有代码的改造,使用 Immutable.js 成本实在太大。
而seamless-immutable
虽然数据结构和API不如Immutable.js
丰富,但是对于只想使用Immutable Data来对React进行优化以避免重复渲染的我们来说,已经是绰绰有余了。而且Array和Object原生的方法等都可以直接使用,原有项目改动极小。
React中使用
由于seamless-immutable的实现依赖于ECMAScript 5 和原生的Array、Object天然的兼容性,导致其在React中的使用非常简单,只要注意三点就可以达到效果:
初始化state
初始化state数据的时候,使用Immutable的初始化方式。
import Immutable from 'seamless-immutable';
state: {
orderList: Immutable([]),
}
修改state数据
修改state数据的时候,同样也要注意:
saveOrderList(state, {payload: items}) {
return {...state, orderList: Immutable(items)};
}
shouldComponentUpdate
使用pure-render-decorator,真是方便、快捷又优雅。当然,由于decorator属于ES7的特性,babel还需要自己配置。
import React from 'react';
import pureRender from 'pure-render-decorator';
@pureRender
class OrderListView extends React.Component {
render() {
const {orderList} = this.props;
return (
<div>
{
orderList.map((item) => {
return (
<div key={item.orderNum}>
<div>{item.orderNum}</div>
<div>{item.createTime}</div>
<div>{item.contact}</div>
<hr/>
</div>
);
})
}
</div>
);
}
}
export default OrderListView;
怎么样,传说中的React的SCU的优化就是这么简单,赶紧去试试吧。