引言:在优化React应用时,绝大部分的优化空间在于避免不必要的render——即Virtual DOM节点的生成,这不仅可以节省执行render的时间,还可以节省对DOM节点做Diff的时间。
本文选自《React与Redux开发实例精解》,将会从五点向您介绍如何避免不必要的render。
1.shouldComponentUpdate
React在组件的生命周期方法中提供了一个钩子shouldComponentUpdate,这个方法默认返回true,表示需要重新执行render方法并使用其返回的结果作为新的Virtual DOM节点。通过实现这个方法,并在合适的时候返回false,告诉React可以不用重新执行render,而是使用原有的Virtual DOM 节点,这是最常用的避免render的手段,这一方式也常被很形象地称为“短路”(short circuit)。
shouldComponentUpdate方法会获得两个参数:nextProps及nextState。常见的实现是,将新旧props及state分别进行比较,确认没有改动或改动对组件没有影响的情况下返回false,否则返回true。
如果shouldComponentUpdate使用不当,实现中的判断并不正确,会导致产生数据更新而界面没有更新、二者不一致的bug,“在合适的时候返回false”是使用这个方法最需要注意的点。要在不对组件做任何限制的情况下保证shouldComponentUpdate完全的正确性,需要手工依据每个组件的逻辑精细地对props、state中的每个字段逐一比对,这种做法不具备复用性,也会影响组件本身的可维护性。
所以一般情况下,会对组件及其输入进行一定的限制,然后提出一个通用的shouldComponentUpdate实现。
首先要求组件的render是“pure”的,即对于相同的输入,render总是给出相同的输出。在这样的基础上,可以对输入采用通用的比较行为,然后依据输入是否一致,直接判断输出是否会是一致的。若是,则可以返回false以避免重复渲染。
其次是对组件输入的限制,要求props与state都是不可修改的(immutable)。如果props与state会被修改,那么判断两次render的输入是否相同便无从说起。
最后值得一说的是,“通用的比较行为”的实现。从理论上说,要判断JavaScript中的两个值是否相等,对于基本类型可以通过===直接比较,而对于复杂类型,如Object、Array,===意味着引用比较,即使引用比较结果为false,其内容也可能是一致的,遍历整个数据结构进行深层比较(deep compare)才能得到准确的答案。但是,shouldComponentUpdate是一个会被频繁调用的方法,而深比较是代价很大的行为,如果数据结构较为复杂,进行深比较甚至会不如直接执行一遍render,通过shouldComponentUpdate实现“短路”也就失去了意义。因此一般来说,会采取一个相对可以接受的方案:浅比较(shallow compare)。相比深比较会遍历整个树状结构而言,浅比较最多只遍历一层子节点。即对于下例的两个对象:
const props = { foo, bar };
const nextProps = { foo, bar };
浅比较会对props.foo与nextProps.foo、props.bar与nextProps.bar进行比较(要求严格相等),而不会深入比较props.foo与nextProps.foo的内容。如此,比较的复杂度会大大降低。
2.Mixin与HoC
前面提到,一个普遍的性能优化做法是,在shouldComponentUpdate中进行浅比较,并在判断为相等时避免重新render。PureRenderMixin是React官方提供的实现,采用Mixin的形式,用法如下。
var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div className={this.props.className}>foo</div>;
}
});
Mixin是ES5写法实现的React组件所推荐的能力复用形式,ES6写法的React组件并不支持,虽然你也可以这么做。
import PureRenderMixin from 'react-addons-pure-render-mixin';
class FooComponent extends React.Component {
constructor(props) {
super(props);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}
手动将 PureRenderMixin提供的shouldComponentUpdate方法挂载到组件实例上。但与其这样,不如直接使用另一个React提供的辅助工具shallow-compare。
import shallowCompare from 'react-addons-shallow-compare';
export class FooComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}
上面两种方式本质上是一致的。
另外也有以高阶组件形式提供这种能力的工具,如库recompose提供的pure方法,用法更简单,很适合ES6写法的React组件。
import {pure} from 'recompose';
class FooComponent extends React.Component {
render() {
return <div className={this.props.className}>foo</div>;
}
}
const OptimizedComponent = pure(FooComponent);
与前两种方式不同的是,这种做法也支持函数式组件。
const FunctionalComponent = ({ className }) => (
<div className={className}>foo</div>;
);
const OptimizedComponent = pure(FunctionalComponent);
3.不可变数据
前面提到,为了让这种“短路”的做法产生预期的效果,要求数据(props与state)是不可变的。然而在JavaScript中,数据天生是可变的,修改复杂的数据结构也是很自然的做法。
const a = { foo: { bar: 1} };
a.foo.bar = 2;
但以这种方式修改数据会导致使用了a作为props的组件失去实现shouldComponentUpdate的意义。为此,Facebook的工程师开发了immutable-js用于创建并操作不可变数据结构。典型的使用是如下这样的。
import Immutable from 'immutable';
const map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
使用immutable-js的代价主要有两部分,一方面库本身的体积并不算小(55.7KB,Gzip压缩后16.3KB),另一方面在开发中需要引入一套新的数据操作方式。除了immutable-js外,mori、Cortex等也是可选的方案,但也都有着类似的问题。幸而大部分情况下都可以选择另外一个相对代价较小的做法:使用 JavaScript原生语法或方法中对不可变数据更友好的那些部分。
对于基本数据类型(boolean、number、string 等),它们本身就是不可变的,它们的操作与计算会产生新的值。而对于复杂数据类型,主要是object与array,在修改时需要稍加注意。
对于object,像如下这样的操作方式是会修改原数据本身的。
obj.a = 1;
obj['b'] = 2;
Object.assign(obj, { a: 1 });
而下面这样的操作是不会的。
const newObj = Object.assign({}, obj, { a: 1 });
如果借助Object Rest/Spread Properties的语法(目前处于Stage 2的提案,在未来可能成为标准),还可以如下这么写。
const newObj = { ...obj, { a: 1 } };
对于array,如下这样的操作会修改原数据本身。
arr[0] = 1;
arr.push(2);
arr.pop();
arr.unshift(3);
arr.shift();
arr.splice(0, 1, [2]);
而Array.prototype也提供了很多不会修改原数组的变换方法,它们会返回一个新的数组作为结果。
arr.concat(1);
arr.slice(-1);
arr.map(item => item.name);
arr.filter(item => item.name !== '');
也可以通过增加一步复制数组的行为,然后在新的数组上进行操作。
const newArr = Array.from(arr);
newArr.push(1);
const newArr2 = Array.from(arr);
newArr2[0] = 1;
如果借助ES6的Array Rest/Spread语法,还可以如下这么做。
[...arr, 1];
[...arr.slice(0, -1), 1];
React官方也有提供一个便于修改较复杂数据结构深层次内容的工具——react-addons-update,它的用法借鉴了MongoDB的query语法(示例来自React官方文档)。
var update = require('react-addons-update');
var newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
如上的行为会在myData的基础上创造一个新的对象newData,且newData.x.y.z会被赋值为7,newData.a.b的内容(一个数组)会被push进值9。对比不使用update的写法(示例来自React官方文档)如下。
var newData = extend(myData, { x: extend(myData.x, { y: extend(myData.x.y, {z: 7}), }), a: extend(myData.a, {b: myData.a.b.concat(9)}) });
上例中extend(myData, …) 的行为类似于Object.assign({},myData, …)。可见,在很多场景下,update都是一个非常有用的工具,可以提高代码的简洁性与可读性。
4.计算结果记忆
使用immutable data可以低成本地判断状态是否发生变化,而在修改数据时尽可能复用原有节点(节点内容未更改的情况下)的特点,使得在整体状态的局部发生变化时,那些依赖未变更部分数据的组件所接触到的数据保持不变,这在一定程度上减少了重复渲染。
然而很多时候,组件依赖的数据往往不是简单地读取全局state上的一个或几个节点,而是基于全局state中的数据计算组合出的结果。以一个Todo List应用为例,在全局的state中通过list存放所有项,而组件VisibleList需要展示未完成项。
const stateToProps = state => {
const list = state.list;
const visibleFilter = state.visibleFilter;
const visibleList = list.filter(
item => (item.status === visibleFilter)
);
return {
list: visibleList
};
};
function List({list}) {/* ... */}
const VisibleList = connect(stateToProps)(List);
如上,在方法stateToProps中基于state计算出当前要展示的项列表visibleList,并将其传递给组件List进行展示。有一个潜在的性能问题是,当state的内容变更时,即使state.list与state.filter均未变更,每次执行stateToProps都会计算生成一个新的visibleList数组。这时即便组件List在shouldComponentUpdate方法中对props进行比较,得到的结果也是不相等的,从而触发重新render。
当应用变得复杂时,绝大部分组件所使用的数据都是基于全局state的不同部分,通过各种方式计算处理得到的,这一情况会随处可见,很多基于shouldComponentUpdate的“短路”式优化都会失去效果。
对此,有一个简单的解决方法是记忆计算结果。一般把从state计算得到一份可用数据的行为称为selector。
const visibleListSelector = state => state.list.filter(
item => (item.status === state.visibleFilter)
);
如果这样的selector具备记忆能力,即在其结果所依赖的部分数据未变更的情况下,直接返回先前的计算结果,那么前面提到的问题将迎刃而解。
reselect就是实现了这样一个能力的JavaScript库。它的使用很简单,下面来改写一下上边的几个selector。
import { createSelector } from 'reselect';
const listSelector = state => state.list;
const visibleFilterSelector = state => state.visibleFilter;
const visibleListSelector = createSelector(
listSelector,
visibleFilterSelector,
(list, visibleFilter) => list.filter(
item => (item.status === visibleFilter)
)
);
可以看到,实现了3个selector:listSelector、visibleFilterSelector及visibleListSelector,其中visibleListSelector由listSelector与visibleFilterSelector通过createSelector组合而成。即,一个selector可以由一个或多个已有的selector结合一个计算函数组合得到,其中组合函数的参数就是传入的几个selector的结果。reselect的价值不仅在于提供了这种组合selector的能力,而且通过createSelector组合产生的selector具有记忆能力,即除非计算函数有参数变更,否则它不会被重新执行。也就是说,除非state.list或state.visibleFilter发生变化,visibleListSelector才会返回新的结果,否则visibleListSelector会一直返回同一份被记忆的数据。
可见,类似reselect这样的方案帮助解决了基于原始state的计算结果比较的问题,有助于实现shouldComponentUpdate来提升应用性能。同时,将基于state的计算行为以统一的形式实现并组装,也有助于复用逻辑,提高应用的可维护性。
5.容易忽视的细节
最后,在组件的实现中,一些很容易被忽视的细节,会趋于让相关组件的shouldComponentUpdate失效,给性能带来潜在的风险。它们的特点是,对于相同的内容,每次都创造并使用一个新的对象/函数,这一行为存在于前面提到的selector之外,典型的位置包括父组件的render方法、生成容器组件的stateToProps方法等。下面是一些常见的例子。
- 函数声明
经常在render中声明函数,尤其是匿名函数及ES6的箭头函数,用来作为回调传递给子节点,一个典型的例子如下。
const onItemClick = id => console.log(id);
function List({list}) {
const items = list.map(
item => (
<Item key={item.id} onClick={() => onItemClick(item.id)}>{item.name}</Item>
)
);
return (
<p>{items}</p>
);
}
如上,希望监听列表每一项的点击事件,获取当前被点击的项的ID,很自然地,在render 中为每个item创建了箭头函数作为其点击回调。这会导致每次组件BtnList的render都会重新生成一遍这些回调函数,而这些回调函数是子节点Item的props的组成,从而子节点不得不重新渲染。
函数绑定
- 函数声明
与函数声明类似,函数绑定(Function.prototype.bind)也会在每次执行时产生一个新的函数,从而影响使用方对props的比对。
函数绑定的使用场景有两种,一是为函数绑定上下文(this),如下。
class WrappedInput extends React.Component {
// ……
onChange(e) {
//在此添加回调代码
}
render() {
return (
<Input onChange={this.onChange.bind(this)} />
);
}
//……
}
这种情况一般出现在ES6写法的React组件中,因为通过ES5的写法React.createClass创建的组件,在被实例化时,其原型上的方法会被统一绑定到实例本身。因此对于这种情况,通常建议参考ES5写法的组件的做法,将bind行为提前,即在实例化时将需要绑定的方法进行手动绑定。
class WrappedInput extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this); }
//……
onChange(e) {
// do some stuff……}
render() {
return ( ); } //……}
这样bind只需执行一次,每次render传入给子组件Input的都是同一个方法。
二是为函数绑定参数,在父组件的同一个方法需要给多个子节点使用时尤为常见,如下。
class List extends React.Component {
onRemove(id) {
//在此添加回调代码
}
render() {
const items = this.props.items.map(
item => (
<Item key={item.id} onRemove={this.onRemove.bind(this, item.id)}>
{item.name}
</Item>
)
);
return (
<section>{items}</section>
);
}
}
对于这个场景最简单的做法是,将bind了上下文的父组件方法onRemove连同item.id传递给子组件,由子组件在调用onRemove时传入item.id,像如下这样。
class Item extends React.Component {
onRemove() {
this.props.onRemove(this.props.id);
}
render() {
//在此this.onRemove方法
}
}
class List extends React.Component {
constructor(props) {
super(props);
this.onRemove = this.onRemove.bind(this);
}
onRemove(id) {}
render() {
const items = this.props.items.map(
item => (
<Item key={item.id} onRemove={this.onRemove} id={id}>
{item.name}
</Item>
)
);
return (
<section>{items}</section>
);
}
}
但不得不承认的是,对于子组件Item来说,拿到一个通用的onRemove方法是不太合理的。所以会有一些解决方案采取这样的思路:提供一个具有记忆能力的绑定方法,对于相同的参数,返回相同的绑定结果。或者借助React组件记忆先前render结果的特点,将绑定行为实现为一个组件,Saif Hakim在文章《Performance EngineeringWith React》中介绍了一种这样的实现,感兴趣的读者可以了解一下。
笔者的观点是,绝大部分情况下,都不至于需要为了性能做这么多的妥协。除非极端情况,否则代码的简洁、可读要比性能更重要。对于这种情况,已知的解决方法或者会影响应用逻辑分布的合理性,或者会引入过多的复杂度,这里提出仅供参考,实际的必要性需要结合具体项目分析。
- object/array字面量
代码中的对象与数组字面量是另一处“新数据”的源头,它们经常表现为如下样式。
function Foo() {
return (
<Bar options={['a', 'b', 'c']} />
);
}
处理这种情况,只需将字面量保存在常量中即可,如下。
const OPTIONS = ['a', 'b', 'c'];
function Foo() {
return (
<Bar options={OPTIONS} />
);
}
本文选自《React与Redux开发实例精解》,点此链接可在博文视点官网查看。
相关推荐
10. **性能优化**:React通过虚拟DOM(Virtual DOM)和差异算法(Reconciliation)提高了性能,避免了不必要的DOM操作。此外,PureComponent、shouldComponentUpdate、React.memo等技术也能帮助优化性能。 以上就是...
1. shouldComponentUpdate:自定义此生命周期方法,避免不必要的渲染。 2. PureComponent和React.memo:利用浅比较优化组件渲染,减少不必要的DOM操作。 3. 使用Fragment:避免无意义的根元素,提高性能。 4. 使用懒...
3. 使用PureComponent和shouldComponentUpdate优化渲染:避免不必要的渲染,提高性能。 4. 高阶组件(HOC):通过高阶组件可以复用组件逻辑,实现如数据注入、权限控制等功能。 5. Hooks:React 16.8引入的Hooks(如...
本实践项目聚焦于`useMemo`和`useCallback`这两个重要的优化钩子,它们能帮助我们减少不必要的重渲染,提升组件性能。以下是对这两个钩子的详细解释以及如何在实际应用中使用它们进行优化。 **useMemo** `useMemo`...
9. **测试与性能优化**:了解如何使用Jest和Enzyme进行React应用的测试,以及如何优化React应用的性能,比如通过shouldComponentUpdate避免不必要的渲染。 10. **最佳实践**:学习React社区推荐的最佳编码实践,以...
- 使用PureComponent或shouldComponentUpdate来避免不必要的渲染。 - 使用React.memo进行函数组件的浅比较,减少不必要的重渲染。 - 利用React的context API来减少props的传递,提高性能。 - 使用代码分割和懒...
- **PureComponent/shouldComponentUpdate**:优化组件渲染,防止不必要的重渲染。 - **React.memo**:函数组件的版本,如果props不变,避免组件重复渲染。 - **代码分割**:利用Webpack的动态导入功能,按需加载...
- **虚拟DOM**:React使用虚拟DOM来提高性能,它在内存中创建一个DOM的抽象表示,当组件状态改变时,React计算出最小的DOM更新,避免了不必要的DOM操作。 2. **React组件生命周期**: - React组件有三个主要生命...
通过学习React的这些内部机制,开发者能够更好地优化应用性能,避免不必要的DOM操作,以及更有效地调试和解决问题。 在后续章节中,我们将深入探讨React的Fiber架构、`workInProgress`树的构建过程、`Effect List`...
6. **性能优化**:React通过`shouldComponentUpdate`、`React.PureComponent`、`React.memo`以及`key`属性等手段优化组件的渲染性能,避免不必要的重渲染。 7. ** fiber架构**:React 16引入了Fiber架构,这是一种...
在React应用程序中,首次渲染是启动应用的关键步骤,它涉及到一系列复杂的操作,这些操作使得React能够将组件结构转化为实际的DOM...在实际开发中,理解这些内部流程有助于优化性能,减少不必要的渲染,提升用户体验。
15. shouldComponentUpdate和React.memo:通过这些方法或HOC,可以避免不必要的组件重新渲染,提高应用性能。 16. PureComponent和React.memo的差异:PureComponent对props和state进行浅比较,而React.memo则适用于...
理解虚拟DOM的工作原理,以及`shouldComponentUpdate`和`React.PureComponent`如何帮助减少不必要的渲染。 4. 渲染和更新:学习如何使用`ReactDOM.render()`将组件挂载到实际DOM中,以及如何在组件更新时触发重渲染...
1. **PureComponent/React.memo**:为了提高性能,可以使用PureComponent或React.memo来避免不必要的组件重新渲染,它们会对props进行浅比较。 2. **代码分割与懒加载**:对于大型应用,可以使用动态导入和代码分割...
- **PureComponent/shouldComponentUpdate**:通过浅比较 state 和 props 来避免不必要的渲染。 - **React.memo**:用于函数组件的浅比较,与 PureComponent 类似。 - **Fragment, Portal, Suspense**:React 新...
1. **PureComponent和shouldComponentUpdate**:React中的PureComponent是React.Component的一个优化版本,它会对props和state进行浅比较,如果两者没有变化,则避免不必要的渲染。然而,对于更复杂的数据结构,可以...
4. **虚拟DOM**:React使用虚拟DOM来优化性能,避免每次状态变化时都直接操作实际DOM,从而减少不必要的DOM操作。 5. **状态提升和高阶组件**:当多个组件需要共享状态时,可以使用状态提升或者高阶组件(HOC)来...
- 使用 `shouldComponentUpdate` 或 `React.memo` 避免不必要的渲染。 - 利用 `React.PureComponent` 或 `React.memo` 进行浅比较,减少子组件的渲染。 - 使用 `key` 属性帮助 React 更好地跟踪列表项的变化。 ...
- **虚拟DOM**: 虚拟DOM是React优化性能的关键,它在内存中创建一个DOM的抽象表示,通过比较新旧虚拟DOM树的差异,只做必要的DOM更新,避免了不必要的DOM操作。 - **状态管理**: 对于复杂应用,单一组件的状态管理...