引言:在优化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开发实例精解》

  
 

React应用优化:避免不必要的render相关推荐

  1. React性能优化之Context

    先来说说Context的作用 react之类的mvvm框架有个痛点想必大家都遇到过,那就是祖孙组件之间的通信 现在组件之间的通信规范是: 父 -> 子:属性绑定props 子 -> 父:事 ...

  2. react 组件遍历】_从 Context 源码实现谈 React 性能优化

    (给前端大全加星标,提升前端技能) 转自:魔术师卡颂 学完这篇文章,你会收获: 了解Context的实现原理 源码层面掌握React组件的render时机,从而写出高性能的React组件 源码层面了解 ...

  3. React性能优化记录(不定期更新)

    React性能优化记录(不定期更新) 1. 使用PureComponent代替Component 在新建组件的时候需要继承Component会用到以下代码 import React,{Componen ...

  4. React性能优化总结

    文章同步于Github Pines-Cheng/blog 初学者对React可能满怀期待,觉得React可能完爆其它一切框架,甚至不切实际地认为React可能连原生的渲染都能完爆--对框架的狂热确实会 ...

  5. React性能优化SCU | PureComponent | memo

    文章目录 React性能优化SCU React更新机制 render函数被调用 PureComponent 高阶组件memo React性能优化SCU React更新机制 我们在前面文章已经讲解过Re ...

  6. React 性能优化完全指南,将自己这几年的心血总结成这篇!

    作者: MoonBall 原文地址: https://juejin.cn/post/6935584878071119885 本文分为三部分,首先介绍 React 的工作流,让读者对 React 组件更 ...

  7. 将 React 应用优化到 60fps

    将 React 应用优化到 60fps翻译自React at 60fps,从属于Web 前端入门与工程实践. 作为 DOM 的抽象,React 自然也遵循了著名的抽象漏洞定理(详见2016-我的前端之 ...

  8. Airbnb 爱彼迎房源详情页中的 React 性能优化

    Airbnb 爱彼迎工程师和数据科学家将定期和大家分享移动开发.系统架构.数据科学及人工智能等领域的技术探索和经验心得. 正文从这开始-- 在一些容易被忽视但又非常重要的场景,可能会有许多严重影响性能 ...

  9. React性能优化(完整版)

    我的博客 http://wangxince.site/my-demo-markdown/ React 性能优化 1.减少 render 次数 shouldComponentUpdate PureCom ...

  10. React router 的 Route 中 component 和 render 属性理解

    React router 的 Route 中 component 和 render 属性理解 Route 标签的三个互斥属性 render.component.children Route 就是用来匹 ...

最新文章

  1. OpenCASCADE:绘制测试线束之入门
  2. 实战例子_Pytorch官方力荐新书《Pytorch深度学习实战指南》pdf及代码分享
  3. python初学篇笔记_Python学习笔记(基础篇)
  4. git+pylint实现python提交代码格式校验
  5. jquery 请求jsp传递json数据的方法
  6. Go实现简单负载均衡
  7. Linux 变量和结构体
  8. java转动的风扇课程设计,课程设计—智能风扇设计报告
  9. 也用 Log4Net 之走进Log4Net (四)
  10. 神经网络拟合高程异常
  11. C9:Unity3D制作智能家居设计软件——导入户型图自动设计(算法剖析+源码实现篇)
  12. 录制Gif动画的软件-ScreenToGif
  13. css3揭秘读书笔记--边框内圆角
  14. CDN是什么意思 CDN加速服务有什么功能和作用?
  15. Flink链接kafka并解析Json文件(三)
  16. 大脑记忆系统研究取得重大进展:或被用于开发新的芯片和操作系统
  17. 免费抠图神器!五秒在线搞定抠图
  18. 赛普拉斯CYpress,初接触之一电磁感应触摸按键demo
  19. 程序员的核心竞争力是什么?
  20. 设计模式 考试题+答案

热门文章

  1. mysql补0操作有什么意义?
  2. 第二届360杯全国大学生信息安全技术大赛_几道小题解析
  3. php curl 伪造IP来源的代码分享
  4. 利用openssl进行base64的编码与解码
  5. LC-1186 连续子数组中可删除一个数的最大和
  6. Vue的单页应用中如何引用单独的样式文件
  7. Win7 + VS2015 + CMake3.6.1-GUI + Makefile 编译开源库
  8. 使用自定义条件触发Nintex workflow
  9. ubuntu - 14.04,如何使用鼠标右键菜单在shell中打开选择项目?
  10. Linux系统下编译连接C源代码