这一次,理解透彻常用React性能优化

1,770 阅读12分钟

性能优化--类组件和函数组件

类组件性能优化

PureComponent

React.PureComponent 与 React.Component 用法差不多 ,但React.PureComponent通过props和state的浅对比来判断是否应该重新render。首先来看一下 PureComponent 的基本使用。

/* Children组件 */
class Children extends React.PureComponent {
  state = {
    name: "jimmy",
    age: 18,
    obj: {
      number: 1,
    },
  };
  changeObjNumber = () => {
    const { obj } = this.state;
    obj.number++;
    // this.setState({ obj: { ...obj } }); // 浅拷贝改变地址指针
    this.setState({ obj });
  };
  render() {
    console.log("组件渲染");
    return (
      <div>
        <div> 组件本身改变state </div>
        <button onClick={() => this.setState({ name: "chimmy" })}>
          state相同情况
        </button>
        <button onClick={() => this.setState({ age: this.state.age + 1 })}>
          state不同情况
        </button>
        <button onClick={this.changeObjNumber}>state为引用数据类型时候</button>
      </div>
    );
  }
}
/* 父组件 */
export default function Home() {
  const [numberA, setNumberA] = React.useState(0);
  const [numberB, setNumberB] = React.useState(0);
  return (
    <div>
      <div> 父组件改变props </div>
      <button onClick={() => setNumberA(numberA + 1)}>改变numberA</button>
      <button onClick={() => setNumberB(numberB + 1)}>改变numberB</button>
      <Children number={numberA} />
    </div>
  );
}

效果

xguo.gif

  • 对于传入props ,PureComponent会浅比较props 是否发生改变,再决定是否渲染组件,所以只有点击 numberA改变传入的numberA才会促使组件重新渲染。
  • 对于自身的state ,也会浅比较处理,当上述触发state 相同情况 按钮时,组件没有渲染。因为state没有变化。
  • 浅比较只会比较基础数据类型,对于引用类型,比如 demo 中 state 的 obj ,单纯的改变 obj 下属性是不会促使组件更新的,因为浅比较两次 obj 还是指向同一个内存空间,想要解决这个问题也容易,浅拷贝就可以解决,将如上 changeObjNumber 这么修改。这样就是重新创建了一个 obj ,所以浅比较会不相等,组件就会更新了。
changeObjNumber = () => {
    const { obj } = this.state;
    obj.number++;
    this.setState({ obj: { ...obj } });
  };

PureComponent进行浅比较的一些源码如下: 当选择继承PureComponent时。原型链上会设置一个isPureReactComponent为true的属性。

pureComponentPrototype.isPureReactComponent = true;
// 用浅比较来比较 props/state
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      //浅比较的判断
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

可以看到,浅比较是用的shallowEqual这个函数,对比新旧的state和props。shallowEqual源码如下:

//true 为不要更新  
//false 为要更新

function shallowEqual(objA: mixed, objB: mixed): boolean {
  // is 同 Object.js(),注意对象只会比较指针.
  // 比较是否相等,如果相等,return true 不更新
  if (is(objA, objB)) {
    return true;
  }
  //只要有一个不是 object或为 null 则返回 false,要更新
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  //两个 object 的key 数不一样,则返回 false,要更新
  if (keysA.length !== keysB.length) {
    return false;
  }
  // Test for A's keys different from B.
  //每一个 value 去一一比较是否是浅相等
  //能执行到这里,说明两者 key 的长度是相等的
  for (let i = 0; i < keysA.length; i++) {
    if (
      //不通过原型链查找是否有自己的属性
      !hasOwnProperty.call(objB, keysA[i]) ||
      //判断两值是否相等
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      //只要没有属性/两个value不等,则返回 false,需要更新
      return false;
    }
  }
  //默认返回 true,不需要更新
  return true;
}
export default shallowEqual;

shallowEqual 浅比较总结

  • 1,首先会直接比较新老 props 或者新老 state 是否相等。如果相等那么不更新组件。
  • 2,判断新老 state 或者 props ,有不是对象,或者为 null 的,那么直接返回 false ,更新组件。
  • 3,通过 Object.keys 将新老 props 或者新老 state 的属性名 key 变成数组,判断数组的长度是否相等,如果不想等,证明有属性增加或者减少,那么更新组件。
  • 4,遍历老 props 或者老 state ,判断对应的新 props 或新 state ,有没有与之对应并且相等的(这个相等是浅比较),如果有一个不对应或者不想等,那么直接返回 false ,更新组件。

到此为止,浅比较流程结束, PureComponent 就是这么做性能优化的。

PureComponent注意事项
PureComponent 可以让组件自发的做一层性能上的调优,但是,父组件给是 PureComponent 的子组件绑定事件要格外小心,避免两种情况发生:

1 避免使用箭头函数。不要给 PureComponent 子组件绑定箭头函数,因为父组件每一次 render ,如果是箭头函数绑定的话,都会重新生成一个新的箭头函数, PureComponent 对比新老 props 时候,因为是新的函数,所以会判断不相等(浅比较第一步,函数是对象,只会判断引用的指针,不相等,直接返回false),而让组件直接渲染,PureComponent 作用终会失效。

class Child extends React.PureComponent{} 

export default class Father extends React.Component{ 
    render=()=> <Child callback={()=>{}} /> 
}

2.PureComponent 的父组件是函数组件的情况,绑定函数要用 useCallback 或者 useMemo 处理。这种情况还是很容易发生的,就是在用 class + function 组件开发项目的时候,如果父组件是函数,子组件是 PureComponent ,那么绑定函数要小心,因为函数组件每一次执行,如果不处理,还会声明一个新的函数,所以 PureComponent 对比同样会失效,如下情况:

  class Index extends React.PureComponent{}
  export default function (){
  /* 每一次函数组件执行重新声明一个新的callback,PureComponent浅比较会认为不想等,促使组件更新  */
      const callback = function handerCallback(){} 
      return <Index callback={callback}  />
  }

综上可以用 useCallback 或者 useMemo 解决这个问题,useCallback 首选,这个 hooks 初衷就是为了解决这种情况的。

export default function (){
    const callback = React.useCallback(function handerCallback(){},[])
    return <Index callback={callback}  />
}

这里简单了解useCallback就行,后面会详细讲到useCallback和useMemo。

shouldComponentUpdate

有的时候,把控制渲染,性能调优交给 React 组件本身处理显然是靠不住的,React 需要提供给使用者一种更灵活配置的自定义渲染方案,使用者可以自己决定是否更新当前组件,shouldComponentUpdate 就能达到这种效果。接下来看一下 shouldComponentUpdate 如何使用。

class Index extends React.Component {
  //子组件
  state = {
    stateNumA: 0,
    stateNumB: 0,
  };
  shouldComponentUpdate(newProp, newState, newContext) {
    if (
      newProp.propsNumA !== this.props.propsNumA ||
      newState.stateNumA !== this.state.stateNumA
    ) {
      return true; /* 只有当 props 中 propsNumA 和 state 中 stateNumA 变化时,更新组件  */
    }
    return false;
  }
  render() {
    console.log("组件渲染");
    const { stateNumA, stateNumB } = this.state;
    return (
      <div>
        <button onClick={() => this.setState({ stateNumA: stateNumA + 1 })}>
          改变state中numA
        </button>
        <button onClick={() => this.setState({ stateNumB: stateNumB + 1 })}>
          改变stata中numB
        </button>
        <div>hello,let us learn React!</div>
      </div>
    );
  }
}
export default function Home() {
  // 父组件
  const [numberA, setNumberA] = React.useState(0);
  const [numberB, setNumberB] = React.useState(0);
  return (
    <div>
      <button onClick={() => setNumberA(numberA + 1)}>改变props中numA</button>
      <button onClick={() => setNumberB(numberB + 1)}>改变props中numB</button>
      <Index propsNumA={numberA} propsNumB={numberB} />
    </div>
  );
}

111.gif

shouldComponentUpdate 可以根据传入的新的 props 和 state ,或者 newContext 来确定是否更新组件,如上面例子,只有当 props 中 propsNumA 属性和 state 中 stateNumA 改变的时候,组件才渲染。但是有一种情况就是如果子组件的 props 是引用数据类型,比如 object ,还是不能直观比较是否相等。那么如果想要对比新老属性相等,怎么对比呢,而且很多情况下,组件中数据可能来源于服务端交互,对于属性结构是未知的。

immutable.js 可以解决此问题,immutable.js 不可变的状态,对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。鉴于这个功能,所以可以把需要对比的 props 或者 state 数据变成 Immutable 对象,通过对比 Immutable 是否相等,来证明状态是否改变,从而确定是否更新组件。另外,还有一个更好用的 immer.js也能解决这个问题。

注意:使用了shouldComponentUpdate,会代替PureComponent的浅比较.原因如下:

源码中,判断到shouldComponentUpdate为function时,会优先执行里面的逻辑。

function checkShouldComponentUpdate(){
     if (typeof instance.shouldComponentUpdate === 'function') {
         /* shouldComponentUpdate逻辑 */
         return instance.shouldComponentUpdate(newProps,newState,nextContext)  
     } 
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return  !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    }
}

函数组件性能优化

React.memo

React.memo(Component,compare)

React.memo实际上是一个高阶组件,它会通过对比 props 变化,来决定是否渲染组件,首先先来看一下 memo 的基本用法。React.memo 接受两个参数,第一个参数 Component 原始组件本身,第二个参数 compare 是一个函数,可以根据一次更新中 props 是否相同决定原始组件是否重新渲染。

memo的几个特点是:

  • React.memo: 第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。和 shouldComponentUpdate 相反,shouldComponentUpdate : 返回 true 组件渲染 , 返回 false 组件不渲染。
  • memo 当二个参数 compare 不存在时,会用浅比较原则处理 props ,相当于仅比较 props 版本的 pureComponent。
  • memo 同样适合类组件和函数组件。实际中,一般只用于函数组件

接下来做一个小案例,利用 memo 做到自定义 props 渲染。 规则: 控制 props 中的 number 。

  • 1 只有 number 更改,组件渲染。
  • 2 只有 number 小于 5 ,组件渲染。
import React, { memo } from "react";
function TextMemo(props) {
  console.log("子组件渲染");
  return <div>hello,world</div>;
}
const controlIsRender = (pre, next) => {
  return (
    pre.number === next.number ||
    (pre.number !== next.number && next.number > 5)
  ); // number不改变或number 改变但值大于5->不渲染组件 | 否则渲染组件
};
const NewTexMemo = memo(TextMemo, controlIsRender);
class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 1,
      num: 1,
    };
  }
  render() {
    const { num, number } = this.state;
    return (
      <div>
        <div>
          改变num:当前值 {num}
          <button onClick={() => this.setState({ num: num + 1 })}>num++</button>
          <button onClick={() => this.setState({ num: num - 1 })}>num--</button>
        </div>
        <div>
          改变number: 当前值 {number}
          <button onClick={() => this.setState({ number: number + 1 })}>
            {" "}
            number ++
          </button>
          <button onClick={() => this.setState({ number: number - 1 })}>
            {" "}
            number --{" "}
          </button>
        </div>
        <NewTexMemo num={num} number={number} />
      </div>
    );
  }
}

222.gif

useCallback

直接来个例子

import React, { useState } from "react";
// 子组件
function Childs(props) {
  console.log("子组件渲染了");
  return (
    <>
      <button onClick={props.onClick}>改标题</button>
      <h1>{props.name}</h1>
    </>
  );
}
const Child = React.memo(Childs);
function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");
  const callback = () => {
    setTitle("标题改变了");
  };
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
      <Child onClick={callback} name="桃桃" />
    </div>
  );
}

执行结果如下图 image.png

当我点击改副标题这个 button 之后,副标题会变为「副标题改变了」,并且控制台会再次打印出子组件渲染了,这就证明了子组件重新渲染了,但是子组件没有任何变化,那么这次 Child 组件的重新渲染就是多余的,那么如何避免掉这个多余的渲染呢?

找原因

我们在解决问题的之前,首先要知道这个问题是什么原因导致的?

咱们来分析,一个组件重新重新渲染,一般三种情况:

  1. 要么是组件自己的状态改变
  2. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变
  3. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变 接下来用排除法查出是什么原因导致的:

第一种很明显就排除了,当点击改副标题 的时候并没有去改变 Child 组件的状态;

第二种情况,我们这个时候用 React.memo 来解决了这个问题,所以这种情况也排除。

那么就是第三种情况了,当父组件重新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name,一个是 onClickname 是传递的常量,不会变,变的就是 onClick 了,为什么传递给 onClick 的 callback 函数会发生改变呢?其实在函数式组件里每次重新渲染,函数组件都会重头开始重新执行,那么这两次创建的 callback 函数肯定发生了改变,所以导致了子组件重新渲染。

用useCallback解决问题

const callback = () => {
  doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])

把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。

那么只需这样将传给Child组件callback函数的改造一下就OK了

const callback = () => { setTitle("标题改变了"); };
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
<Child onClick={useCallback(callback, [])} name="桃桃" />

这样我们就可以看到只会在首次渲染的时候打印出子组件渲染了,当点击改副标题和改标题的时候是不会打印子组件渲染了的。

useMemo

const cacheSomething = useMemo(create,deps)

  • create:第一个参数为一个函数,函数的返回值作为缓存值。
  • deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
  • cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

useMemo原理:

useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。

来看下面的例子

function Child(){
    console.log("子组件渲染了")
    return <div>Child</div> 
}
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = {
      age: count,
      name: 'jimmy'
    }
    return <Child userInfo={userInfo}/>
}

当函数组件重新render时,userInfo每次都将是一个新的对象,无论 count 发生改变没,都会导致 Child组件的重新渲染。

而下面的则会在 count 改变后才会返回新的对象。

function Child(){
    console.log("子组件渲染了")
    return <div>Child</div> 
}
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = useMemo(() => {
      return {
        name: "jimmy",
        age: count
      };
    }, [count]);
    return <Child userInfo={userInfo}>
}

实际上 useMemo 的作用不止于此,根据官方文档内介绍:以把一些昂贵的计算逻辑放到 useMemo 中,只有当依赖值发生改变的时候才去更新。

import React, {useState, useMemo} from 'react';

// 计算和的函数,开销较大
function calcNumber(count) {
  console.log("calcNumber重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}
export default function MemoHookDemo01() {
  const [count, setCount] = useState(100000);
  const [show, setShow] = useState(true);
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);
  return (
    <div>
      <h2>计算数字的和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>show切换</button>
    </div>
  )
}

当我们去点击 show切换按钮时,calcNumber这个计算和的函数并不会出现渲染了.只有count 发生改变时,才会出现计算.

useCallback 和 useMemo 总结

简单理解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回就结果。useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 可以优化当前组件也可以优化子组件,优化当前组件主要是通过 memoize 来将一些复杂的计算逻辑进行缓存。当然如果只是进行一些简单的计算也没必要使用 useMemo。

我们可以将 useMemo 的返回值定义为返回一个函数这样就可以变通的实现了 useCallback。

这里最后总结一张渲染控制流程图

image.png

渲染中keys的作用

key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。在ReactDiff算法中React会借助元素的Key值来判断该元素是新创建的还是被移动而来的元素,React会保存这个辅助状态,从而减少不必要的元素渲染.此外,React还需要借助Key值来判断元素与本地状态的关联关系,因此我们在开发中不可忽视Key值的使用。

一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key

当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key。用索引作为key,对性能时完全没有任何优化的。

Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。

一些思考

有没有必要在乎组件不必要渲染。

在正常情况下,无须过分在乎 React 没有必要的渲染,要理解执行 render 不等于真正的浏览器渲染视图,render 阶段执行是在 js 当中,js 中运行代码远快于浏览器的 Rendering 和 Painting 的,更何况 React 还提供了 diff 算法等手段,去复用真实 DOM 。

什么时候需要注意渲染节流的性能优化。

  • 第一种情况数据可视化的模块组件(展示了大量的数据),这种情况比较小心因为一次更新,可能伴随大量的 diff ,数据量越大也就越浪费性能,所以对于数据展示模块组件,有必要采取 memo , shouldComponentUpdate 等方案控制自身组件渲染。
  • 第二种情况含有大量表单的页面,React 一般会采用受控组件的模式去管理表单数据层,表单数据层完全托管于 props 或是 state ,而用户操作表单往往是频繁的,需要频繁改变数据层,所以很有可能让整个页面组件高频率 render 。
  • 第三种情况就是越是靠近 app root 根组件越值得注意,根组件渲染会波及到整个组件树重新 render ,子组件 render ,一是浪费性能,二是可能执行 useEffect ,componentWillReceiveProps 等钩子,造成意想不到的情况发生。