本文总结了一些编写 React 代码的建议,有助于理解 React 的渲染机制,主要是通过减少不必要的重新渲染来提升 React 性能。
回顾使用 React 开发的经历,已有接近两年了,一直想要写一篇文章来介绍如何编写高性能的 React 代码,又或者是性能调优之类的,于是,我总结了一些编写高性能 React 代码的实践,或许能让你对 React 的理解更加深刻。
影响 React 性能的主要因素#
React 专注于构建用户界面,内部通过 VirtualDOM 和 Diff 算法实现视图的更新,本身拥有很高的性能。当组件的 state 和 props 改变的时候,React 就会重新 render,进而更新视图。在 render 过程中,react 会将 JSX 语法转换为虚拟 DOM,同时进行 Diff 算法比较新旧 DOM 的变化,从而对视图进行局部更新。试想,当 render 执行后,JSX 转换虚拟 DOM,然后 Diff,最后发现新旧 DOM 是一样的,最后并没有更新视图。那么,就导致了不必要的 render,造成了性能的浪费。所以,减少不必要的 render,成为优化 React 性能的主要因素。
state 只与视图有关#
编写 React 组件的时候,你肯定会用到变量,变量可以保存在 this 上,可以保存到 state 中,还可以使用全局变量。那么什么时候该使用 state 呢?答案是只有与视图相关的变量才放在 state 中,与视图无关的可以放在 this 上。如果与视图更新无关的变量放在 state 中,而且这个变量需要频繁的更新,而每次更新都需要重新 render,势必造成性能浪费,使用 this 来存放无疑是更好的选择。
shouldComponentUpdate#
考虑如下代码:
export default class Home extends Component {
state = {
count: 0
}
add = () => {
this.setState({count: 0});
}
render() {
console.log('render');
return (
<div className="App">
<div onClick={this.add}>+++</div>
<p className="App-intro">
{this.state.count}
</p>
</div>
);
}
}
当点击 +++ 触发 add 事时,setState 更新 state,但是新旧 state 完全一样,React 并没有在 Component 组件中进行优化,导致不必要的 render 执行。此时,需要我们手动进行优化,当 state 或者 props 更新后 react 组件的生命周期 shouldComponentUpdate 触发,在 shouldComponentUpdate 中可以对新旧 state 和 props 进行比较,判断组件是否需要重新 render。
shouldComponentUpdate(nextProps, nextState) {
if(nextState.count === this.state.count) {
return false;
}
}
默认情况下 shouldComponentUpdate 返回 true,即始终要重新渲染,此处我们使用 shouldComponentUpdate 进行比较后,返回 false,react 将不会重新 render。
当 state 为字符串等基本类型的时候,shouldComponentUpdate 还能应付自如,那如果 state 是对象或者数组等引用类型呢?对于引用类型只能对其进行递归比较才能判断其是否相等,又或者是采用 JSON.stringify (nextState.obj) === JSON.stringify (this.state.obj) 进行比较,但是当对象嵌套层级较深或者函数、正则等类型时,shouldComponentUpdate 便失去了它的用武之地。
PureComponent#
React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent 通过 prop 和 state 的浅对比来实现 shouldComponentUpate ()。
简而言之,PureComponent 自己内部实现了一个 shouldComponentUpdate,而不需要我们再去编写比较代码。
React.PureComponent 的 shouldComponentUpdate () 只会对对象进行浅对比。如果对象包含复杂的数据结构,它可能会因深层的数据不一致而产生错误的否定判断 (表现为对象深层的数据已改变视图却没有更新)。
测试表明,使用 PureComponent 的性能会略胜 shouldComponentUpdate 一筹。
StateLessComponent#
无状态组件(StateLessComponent)又称函数式组件。
const Text = ({ children = 'Hello World!' }) =>
<p>{children}</p>
函数式组件没有 class 声明,没有生命周期,仅使用 render 方法实现,不存在组件实例化的过程,因此不需要分配多余的内存,从而提升了渲染性能。
箭头函数与 bind#
当我们在组件上绑定事件处理函数的时候,为了能正常访问 state,通常会使用 bind 或者箭头函数。
<Button onClick={this.update.bind(this)} />
又或者
<Button onClick={() => { console.log("Click"); }} />
在以上情况下,bind 每次都返回全新的函数,箭头函数在每次 render 时都会重新分配(和使用 bind 的方式相同),对于组件来说每次绑定的都是新的函数,Button 组件在进行 props 比较的时候会认为前后 props 不一样,造成重新渲染。
比较推荐的写法有两种:
update = () => {
//
}
<Button onClick={this.update} />
或者
constructor(props) {
super(props);
this.update = this.update.bind(this);
}
update() {
//
}
<Button onClick={this.update} />
内联 style#
当我们为组件设置样式的时候,可能会写出如下代码:
<Button style={{width: 100, marginTop: 50 }} />
这种写法,每次 render 时 style 返回的都是全新的对象,我们知道,引用类型比较的是引用地址,每次的结果都是不相等,也会导致重新渲染。
推荐的写法是使用 className。
<Button className="btn" />
属性传递与 []#
当我们为组件设置缺省值的时候:
<RadioGroup options={this.props.options || []} />
如果每次 this.props.options 值都是 null 的话,意味着每次传递给都是字面量数组 [],但字面量数组和 new Array () 效果是一样的,始终生成新的实例,所以表面上看虽然每次传递给组件的都是相同的空数组,其实对组件来说每次都是新的属性,都会引起渲染。所以正确的方式应该将一些常用值以变量的形式保存下来:
const DEFAULT_OPTIONS = [];
<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />
map 与 key#
在 react 中,你应该经常用到 map 来渲染一个列表,当你忘记为每个列表元素设置 key 属性的时候,react 会在控制台发出警告 a key should be provided for list items。
实际上,Keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。因此你应当给数组中的每一个元素赋予一个确定的标识。
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用来自数据的 id 作为元素的 key:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
当元素没有确定的 id 时,你可以使用他的序列号索引 index 作为 key:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
当 todoItems 中数据发生改变的时候,使用索引作为 key 的列表都将重新渲染。所以不建议使用索引来进行排序,因为这会导致渲染变得很慢。
减少在 render 内执行变量#
我们已经知道,当组件的 state 和 props 发生改变的时候,render 都会执行,那么,当 render 中有变量的执行或者函数执行的时候,可能会影响渲染性能,所以,应该减少在 render 内执行变量或者函数。
精简 props 和 state#
从上面我们已经知道,当 props 和 state 为引用类型的时候,react 内部只对 props 和 state 进行浅比较,那么,props 和 state 可以尽量设置为基础数据类型,对象的嵌套层级不要过深,减少 props 的参数数量,使组件的 props 和 state 对象数据尽可能达到扁平化,都可以在一定程度上优化 react 的渲染性能。
总结#
本文讨论了一系列编写 React 代码的原则,旨在从减少重新 render 的方面来提升 react 代码的性能。性能优化一直是一个持久的话题,还可以从很多方面去考虑,比如,使用不可变的数据结构 immutable.js,使用 reselect 来缓存 redux 中的复杂计算,对组件尽可能进行拆分为子组件,都是值得探讨的话题。那么,下次再见。