React应用优化:避免不必要的render
引言:在优化React应用时,绝大部分的优化空间在于避免不必要的render——即Virtual DOM节点的生成,这不仅可以节省执行render的时间,还可以节省对DOM节点做Diff的时间。 1.shouldComponentUpdate React在组件的生命周期方法中提供了一个钩子shouldComponentUpdate,这个方法默认返回true,表示需要重新执行render方法并使用其返回的结果作为新的Virtual DOM节点。通过实现这个方法,并在合适的时候返回false,告诉React可以不用重新执行render,而是使用原有的Virtual DOM 节点,这是最常用的避免render的手段,这一方式也常被很形象地称为“短路”(short circuit)。 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>;
}
}
上面两种方式本质上是一致的。 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原生语法或方法中对不可变数据更友好的那些部分。 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可以低成本地判断状态是否发生变化,而在修改数据时尽可能复用原有节点(节点内容未更改的情况下)的特点,使得在整体状态的局部发生变化时,那些依赖未变更部分数据的组件所接触到的数据保持不变,这在一定程度上减少了重复渲染。 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。 const visibleListSelector = state => state.list.filter(
item => (item.status === state.visibleFilter)
);
如果这样的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会一直返回同一份被记忆的数据。 5.容易忽视的细节最后,在组件的实现中,一些很容易被忽视的细节,会趋于让相关组件的shouldComponentUpdate失效,给性能带来潜在的风险。它们的特点是,对于相同的内容,每次都创造并使用一个新的对象/函数,这一行为存在于前面提到的selector之外,典型的位置包括父组件的render方法、生成容器组件的stateToProps方法等。下面是一些常见的例子。
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的组成,从而子节点不得不重新渲染。
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》中介绍了一种这样的实现,感兴趣的读者可以了解一下。
function Foo() {
return (
<Bar options={['a','b','c']} />
);
}
处理这种情况,只需将字面量保存在常量中即可,如下。 const OPTIONS = ['a','c'];
function Foo() {
return (
<Bar options={OPTIONS} /> ); }
本文选自《React与Redux开发实例精解》
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |