阅读 653

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

性能优化--keys

渲染中keys的作用

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

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

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

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

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

类组件性能优化

PureComponent

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

/* Children组件 */
class Children extends React.PureComponent {
  state = {
    name: "alien",
    age: 18,
    obj: {
      number: 1,
    },
  };
  changeObjNumber = () => {
    const { obj } = this.state;
    obj.number++;
    this.setState({ obj });
  };
  render() {
    console.log("组件渲染");
    return (
      <div>
        <div> 组件本身改变state </div>
        <button onClick={() => this.setState({ name: "alien" })}>
          state相同情况
        </button>
        <button onClick={() => this.setState({ age: this.state.age + 1 })}>
          state不同情况
        </button>
        <button onClick={this.changeObjNumber}>state为引用数据类型时候</button>
        <div>hello,my name is alien,let us learn React!</div>
      </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的属性。

//源码位置 react/src/ReactBaseClasses.js

pureComponentPrototype.isPureReactComponent = true;
复制代码
// 源码位置 react/react-reconciler/ReactFiberClassComponent.js
// 用浅比较来比较 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也能解决这个问题。immutable.js和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

一些思考

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

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

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

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