一文掌握React性能优化

一文掌握React性能优化

1 shouldeComponentUpdate

shouldComponentUpdate应该是提到的优化方法里面提到最多的,使用的也是最多的,如果返回false,就会阻止下面组件的渲染,反之,返回true,就会进行接下来的渲染,默认返回的事true。但是有个问题,shouldComponentUpdate进行的是浅比较,为什么不进行深比较,因为伤不起,深比较的耗费性能可能比每次render的效率还要低。

class Nest extends React.Component {
  shouldComponentUpdate = (nextProps, nextState) => {
    return (
      !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
    )
  }
  render() {
    console.log('inner')
    return <div>Nest</div>
  }
}

为什么进行的事浅比较,我们从源码方面来看一下:

const hasOwn = Object.prototype.hasOwnProperty

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if (keysA.length !== keysB.length) return false

  for (let i = 0; i < keysA.length; i++) {
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return true
}

首先是Object.is的Polyfill,为什么要用这个来比较而不是 == 或者 === 呢?

使用 == 会 出现以下bug:

0 == ' '  // true
null == undefined // true
[1] == true // true

使用 === 会出现以下bug:

+0 === -0 // true,
NaN === NaN // false,

Object.is修复了=== 这两种判断不符合预期的情况,并null,undefined,number,string,boolean做出非常精确的比较,以下几种情况都会返回true。

  • 两个值都是 undefined
  • 两个值都是 null
  • 两个值都是 true 或者都是 false
  • 两个值是由相同个数的字符按照相同的顺序组成的字符串
  • 两个值指向同一个对象
  • 两个值都是数字并且
    • 都是正零 +0
    • 都是负零 -0
    • 都是 NaN
    • 都是除零和 NaN 外的其它同一个数字

当对比的类型为Object的时候并且key的长度相等的时候,浅比较也仅仅是用Object.is()对Object的value做了一个基本数据类型的比较,所以如果key里面是对象的话,就不能进行比较。

const hasOwn = Object.prototype.hasOwnProperty
// 下面就是进行浅比较了, 有不了解的可以提issue, 可以写一篇对比的文章。
function is(x, y) {
  // === 严格判断适用于对象和原始类型。但是有个例外,就是NaN。
  if (x === y) {
    //这个是个例外,为了针对0的不同,譬如 -0 === 0 => true
    // (1 / x) === (1 / y)这个就比较有意思,可以区分正负0, 1 / 0 => Infinity, 1 / -0 => -Infinity
    return x !== 0 || y !== 0 || 1 / x === 1 / y 
  } else {
    // 这个就是针对上面的NaN的情况
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true //这个就是实行了Object.is的功能。实行的是SameValue策略。
  // is方法之后,我们认为他不相等。不相等的情况就是排除了(+-0, NaN)的情况以及可以证明:
  // 原始类型而言: 两个不是同类型或者两个同类型,值不同。
  // 对象类型而言: 两个对象的引用不同。

  
  //下面这个就是,如果objA和objB其中有个不是对象或者有一个是null, 那就认为不相等。
  //不是对象,或者是null.我们可以根据上面的排除来猜想是哪些情况:
  //有个不是对象类型或者有个是null,那么我们就直接返回,认为他不同。其主要目的是为了确保两个都是对象,并且不是null。
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  //如果上面没有返回,那么接下来的objA和objB都是对象了。

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  //两个对象不同,有可能是引用不同,但是里面的内容却是相同的。例如:{a: 'a'} ==~ {a: 'a'}
  //所以先简单粗暴的判断一级的keys是不是相同的长度。,不是那就肯定不相等,就返回false。
  if (keysA.length !== keysB.length) return false

  //下面就是判断相同长度的key了
  // 可以发现,遍历的是objA的keysA。
  //首先判断objB是否包含objA的key,没有就返回false。注意这个是采用的hasOwnPrperty来判断,可以应付大部分的情况。
  //如果objA的key也在ObjB的key里,那就继续判断key对应的value,采用is来对比。哦,可以发现,只会对比到第以及。

  for (let i = 0; i < keysA.length; i++) {
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return true
}

2 React.PureComponent

个人觉得此优化手段适用于数据变化不太频繁,如果只有一个或者展示类的组件可以使用,因为在父组件进行重新render时候,可以有效避免利用PureComponent的浅比较避免组件的渲染,不可以有每次都会变动的值,因为这样你的 PureComponent 和 Component 其实没两样。但是一下几个小tip,可能避免组件重新render,其实这样的点有很多,只是把我遇到的全部列举出来。

  • props为空数组
<CustomList data={data || []} />

CustomList 在 data 值为 null 或 undefined 时,仍不会发生奔溃,但如果你这么做,你会发现即使每次传入的 props 都是[],仍然会发生 render ,原因在于[] !== [],他们的 reference 并不相同。解决办法: 设置defaultProps 默认值。

  • props 为 inline function
<CustomList  clickHandler={ () => this.setState({number: this.state.number + 1}) } />

这样 每次点击时,都会传入一个新的function,又因为reference不同,导致每次都会渲染。
解决办法: 将内连的function,进行预定义,拆出来定义。

  • props 使用的function类型 例如:
 1 <CustomList clickHandler={this.handleClick } />   // constructor里面绑定this;
 2 <CustomList clickHandler={this.handleClick.bind(this) } /> 
 3 <CustomList clickHandler={()=>this.handleClick.bind() } /> 

方式一: 构造函数每一次渲染的时候只会执行一遍; 性能最好;

方式二: 在每次render()的时候都会重新执行一遍函数;

方式三: 每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的,因为react判断是否需要进行render是浅层比较,简单来说就是通过===来判断的,如果state或者prop的类型是字符串或者数字,只要值相同,那么浅层比较就会认为其相同

当你使用 PureComponent 時,如果 props 或 state 要变动,可以尝试使用 Immutable.js 来处理,避免有改了却沒重新 render 的情况发生。


3. immutable.js

Immutable提供一直简单快捷的方式以判断对象是否变更,对于React组件更新和重新渲染性能可以有较大帮助。immutable可以几乎在全家桶中结合,打算专门写一篇介绍。

import { is } from 'immutable';

shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
  const props = this.props || {}, state = this.state || {};

  if (Object.keys(props).length !== Object.keys(nextProps).length ||
      Object.keys(state).length !== Object.keys(nextState).length) {
    return true;
  }

  for (const key in nextProps) {
    if (!is(props[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (state[key] !== nextState[key] || !is(state[key], nextState[key])) {
      return true;
    }
  }
  return false;
}

4.单组件中拆分组件,尽量避免嵌套方法,一串传参下去,导致一参数改变,一串函数都会重新render;

5.多组件同样也应该避免从父级获取store数据传给子组件,会导致父组件中的子组件某个数据改变,导致父组件中所有子组件都会改变,全部重新render;

6. 未完待续。。。。

编辑于 2019-05-11

文章被以下专栏收录