React 使用虚拟 DOM,它是在浏览器中的 DOM 子树的渲染描述,这个平行的描述让 React 避免创建和操作 DOM 节点,这些远比操作一个 JavaScript 对象慢。当一个组件的 props 或 state 改变,React 会构造一个新的虚拟 DOM 和旧的进行对比来决定真实 DOM 更新的必要性,只有在它们不相等的时候,React 才会使用尽量少的改动更新 DOM。
在此之上,React 提供了生命周期函数 shouldComponentUpdate
,在重新渲染机制回路(虚拟 DOM 对比和 DOM 更新)之前会被触发,赋予开发者跳过这个过程的能力。这个函数默认返回 true
,让 React 执行更新。
shouldComponentUpdate: function(nextProps, nextState) {
return true;
}
一定要记住,React 会非常频繁的调用这个函数,所以要确保它的执行速度够快。
假如你有个带有多个对话的消息应用,如果只有一个对话发生改变,如果我们在 ChatThread
组件执行 shouldComponentUpdate
,React 可以跳过其他对话的重新渲染步骤。
shouldComponentUpdate: function(nextProps, nextState) {
// TODO: return whether or not current chat thread is
// different to former one.
}
因此,总的说,React 通过让用户使用 shouldComponentUpdate
减短重新渲染回路,避免进行昂贵的更新 DOM 子树的操作,而且这些必要的更新,需要对比虚拟 DOM。
shouldComponentUpdate 实战
这里有个组件的子树,每一个都指明了 shouldComponentUpdate
返回值和虚拟 DOM 是否相等,最后,圆圈的颜色表示组件是否需要更新。
[图片上传失败...(image-225d59-1530447751850)]
在上面的示例中,因为 C2 的 shouldComponentUpdate
返回 false,React 就不需要生成新的虚拟 DOM,也就不需要更新 DOM,注意 React 甚至不需要调用 C4 和 C5 的 shouldComponentUpdate
。
C1 和 C3 的 shouldComponentUpdate
返回 true
,所以 React 需要向下到叶子节点检查它们,C6 返回 true
,因为虚拟 DOM 不相等,需要更新 DOM。最后感兴趣的是 C8,对于这个节点,React 需要计算虚拟 DOM,但是因为它和旧的相等,所以不需要更新 DOM。
注意 React 只需要对 C6 进行 DOM 转换,这是必须的。对于 C8,通过虚拟 DOM 的对比确定它是不需要的,C2 的子树和 C7,它们甚至不需要计算虚拟 DOM,因为 shouldComponentUpdate
。
那么,我们怎么实现 shouldComponentUpdate
呢?比如说你有一个组件仅仅渲染一个字符串:
React.createClass({
propTypes: {
value: React.PropTypes.string.isRequired
},
render: function() {
return <div>{this.props.value}</div>;
}
});
我们可以简单的实现 shouldComponentUpdate
如下:
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value !== nextProps.value;
}
非常好!处理这样简单结构的 props/state 很简单,我门甚至可以归纳出一个基于浅对比的实现,然后把它 Mixin 到组件中。实际上 React 已经提供了这样的实现: PureRenderMixin
但是如果你的组件的 props 或者 state 是可变的数据结构呢?比如说,组件接收的 prop 不是一个像 'bar'
这样的字符串,而是一个包涵字符串的 JavaScript 对象,比如 { foo: 'bar' }
:
React.createClass({
propTypes: {
value: React.PropTypes.object.isRequired
},
render: function() {
return <div>{this.props.value.foo}</div>;
}
});
前面的 shouldComponentUpdate
实现就不会一直和我们期望的一样工作:
// assume this.props.value is { foo: 'bar' }
// assume nextProps.value is { foo: 'bar' },
// but this reference is different to this.props.value
this.props.value !== nextProps.value; // true
这个问题是当 prop 没有改变的时候 shouldComponentUpdate
也会返回 true
。为了解决这个问题,我们有了这个替代实现:
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value.foo !== nextProps.value.foo;
}
基本上,我们结束了使用深度对比来确保改变的正确跟踪,这个方法在性能上的花费是很大的,因为我们需要为每个 model 写不同的深度对比代码。就算这样,如果我们没有处理好对象引用,它甚至不能工作,比如说这个父组件:
React.createClass({
getInitialState: function() {
return { value: { foo: 'bar' } };
},
onClick: function() {
var value = this.state.value;
value.foo += 'bar'; // ANTI-PATTERN!
this.setState({ value: value });
},
render: function() {
return (
<div>
<InnerComponent value={this.state.value} />
<a onClick={this.onClick}>Click me</a>
</div>
);
}
});
内部组件第一次渲染的时候,它会获取 { foo: 'bar' }
作为 value 的值。如果用户点击了 a 标签,父组件的 state 会更新成 { value: { foo: 'barbar' } }
,触发内部组件的重新渲染过程,内部组件会收到 { foo: 'barbar' }
作为 value 的新的值。
这里的问题是因为父组件和内部组件共享同一个对象的引用,当对象在 onClick
函数的第二行发生改变的时候,内部组件的属性也发生了改变,所以当重新渲染过程开始,shouldComponentUpdate
被调用的时候,this.props.value.foo
和 nextProps.value.foo
是相等的,因为实际上 this.props.value
和 nextProps.value
是同一个对象的引用。
因此,我们会丢失 prop 的改变,缩短重新渲染过程,UI 也不会从 'bar'
更新到 'barbar'