React项目开发过程中需要注意避免re-render——性能优化方案

前言:
全是干货!!答应我一定要每字每句,包括代码注释都认认真真看好吗!!都是重点呀~~~

——————————————————————————————————————
首先我们要知道哪些方法会触发react组件的重新渲染?
1、setState方法被调用(hook中是 useState中的setXXXX方法被调用)组件就会触发render,除了设置state为null的情况不会触发render。
注意注意!!上面说的是方法被调用就会re-render,而不指的是state数据发生改变才会re-render。意思就是说如果你点击一个按钮但是一直是 this.setState({ name: ‘winne’ }),那么你点多少次组件就会re-render多少次。

2、父组件重新渲染时,内部的所有子组件默认都会重新渲染,触发render。

重新渲染render会做些什么?
1、会对新l日VNode进行对比,也就是我们所说的DoM diff。
2、对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
3、遍历差异对象,根据差异的类型,根据对应对规则更新VNode
React的处理render的基本思维模式是每次一有变动就会去重新渲染整个应用。在Virtual DOM没有出现之前,最简单的方法就是直接调用innerHTML。Virtual DOM厉害的地方并不是说它比直接操作DOM快,而是说不管数据怎么变,都会尽量以最小的代价去更新DOM。React将render函数返回的虚拟DOM树与老的进行比较,从而确定DOM要不要更新、怎么更新。当DOM树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层setState 一个微小的修改,默认会去遍历整棵树。尽管React使用高度优化的Diff 算法,但是这个过程仍然会损耗性能

现在分react的类组件和hook函数组件进行优化讲解。

一、react class 组件

1、PureComponent和shouldComponentUpdate

使用 PureComponent,每次对 props 进行一次浅比较。当然,除了 PureComponent 外,我们还可以配合 shouldComponentUpdate 生命周期函数进行更深层次的控制。

shouldComponentUpdate来决定组件是否重新渲染,如果不希望组件重新渲染,在逻辑中返回false即可。

react官网性能优化篇幅:点击这里

2、其他的优化点和下面的hook组件有些雷同,只是语法不同,对比一下

二、react hook 组件

1、多挖掘能使用useRef的地方

在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state可以在多次渲染之后依旧不变。但是,state的问题在于一旦修改了它就会造成组件的重新渲染,如果这个组件内部还有很多子组件,那么意味着所有的子组件也会被重新渲染
那么这个时候就可以使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染

注意注意!!
但这并不意味了我们要全部使用useRef,因为你修改了useRef的值视图层是不会重新渲染更新的,所以如果你的变量是决定视图图层渲染的变量,请使用useState。其他用途使用useRef。

相关好文推荐:点击这里

2、正确使用useEffect的第二个参数

useEffect不加第二个参数的时候默认会在每次渲染后都执行。所以我们需要添加第二个参数列表进行控制
第二个参数列表中设置的变量为:每次列表中的变量改变了才会执行useEffect中的逻辑,否则就会跳过。

3、使用React.memo来控制整个函数组件的渲染时机

React.memo是React 16.6新的一个API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与PureComponent十分类似,但不同的是,React.memo只能用于函数组件,我们的hook组件就是一个函数组件。当然 react class中也有函数组件的存在,memo语法相同

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

const Child = (props) => {
    const [value, setValue] = useState(1);
    return (
        <div>
            <span>
                {value}
            </span>
        </div>
    );
};

// 不使用memo时我们一般这样导出组件
// export default Child;

// 使用memo时我们可以这样写
const compareProps = (prevProps, nextProps) => {
    // 这里写入判断渲染的逻辑控制。返回true为不渲染组件,false为渲染组件;
};
// memo接收两个参数,第一个参数是函数组件,第二个参数是比较props从而控制组件是否渲染的函数。
// 如果第二个参数不传递,则默认只会进行 props 的浅比较。
export default memo(Child, compareProps);

相关好文推荐:点击这里

4、使用 useMemo() 进行细粒度性能优化

上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。

而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure 功能。

useMemo() 基本用法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo() 返回的是一个 memoized 值(变量值),只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)。

如果没有提供依赖数组(上面的 [a,b])则每次都会重新计算 memoized 值,也就会 re-render。

memoized 值不变的情况下,不会重新触发渲染逻辑。

说起渲染逻辑,需要记住的是 useMemo() 是在 render 期间执行的,所以传给 useMemo 的函数是在渲染期间运行的。不能进行一些额外的副操作,比如网络请求等。副作用属于 useEffect,而不是 useMemo。

大白话:就是说在 Hook组件中如果你在 return 返回的jsx中使用到了一个变量,这个变量(可以是一个js变量;也可以是jsx组件)需要进行一次很昂贵的计算的话,那么就考虑使用useMemo()来缓存。

下面贴代码方便测试:

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

const Child = (props) => {
    // 函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。
    // 因此useMemo 和useCallback就是解决性能问题的杀手锏。
    console.log(1);
    const [value, setValue] = useState(1);
    const [name, setName] = useState('');

    const initName = () => {
        setName('winne');
    };

    const addValue = () => {
        setValue(value + 1);
    };

    // // 这里模拟了在渲染期间需要进行昂贵的计算得到一个变量(如果不使用useMemo进行缓存,那么无论是name还是value发生变化,这个函数都会被执行)
    // const newVal = () => {
    //     console.log('newVal函数被执行了');
    //     let sum = 0;
    //     for (let i = 0; i < value * 100; i++) {
    //         sum += i;
    //     }
    //     return sum;
    // };

    // 使用useMemo,只有value改变的时候才执行该函数。因为useMemo返回的是一个变量值,所以下面就直接使用newVal
    const newVal = useMemo(() => {
        console.log('newVal函数被执行了');
        let sum = 0;
        for (let i = 0; i < value * 100; i++) {
            sum += i;
        }
        return sum;
    }, [value]);

    // // 这里模拟了渲染期间需要的一个jsx组件(如果不使用useMemo进行缓存,那么无论是name还是value发生变化,这个函数都会被执行)
    // const nameLike = () => {
    //     console.log('nameLike函数被执行了');
    //     return (
    //         <p>
    //             {name} : eat
    //         </p>
    //     );
    // };

    // 使用useMemo,只有name改变的时候才执行该函数。因为useMemo返回的是一个变量值,所以下面就直接使用nameLike
    const nameLike = useMemo(() => {
        console.log('nameLike函数被执行了');
        return (
            <p>
                {name} : eat
            </p>
        );
    }, [name]);

    console.log(2);
    return (
        <div>
            {console.log('re-render')}
            <p>
                name: {name}
            </p>
            <p>
                value: {value}
            </p>
            <p>
                {/* newVal: {newVal()} */}
                newVal: {newVal}
            </p>
            {/* {nameLike()} */}
            {nameLike}
            <Button onClick={addValue}>点击累加value</Button>
            <Button onClick={initName}>点击初始化name</Button>
        </div>
    );
};

export default Child;

阅读文章推荐:点击这里

5、useCallback

useCallback跟useMemo比较类似,但它返回的是缓存的函数。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useCallback() 基本用法如下:

const fnA = useCallback(fnB, [a, b]) 

useCallback() 返回一个 memoized 回调函数。只有当依赖项(比如上面的 a,b 发生变化的时候,该回调函数才会更新)。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

如果没有提供依赖数组(上面的 [a,b])则每次渲染都会重新更新该回调函数,也就会 re-render。

下面我们来看两个例子:
1、只使用useCallback的情况

import React, { useState, useCallback, useEffect } from 'react';

export default function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    // 这里每次父组件渲染时,callbackCount都会发生变化
    // const callbackCount = () => count * 2;

    // 这里使用了useCallback来缓存count函数。只有当count发生变化时,callbackCount这个函数才会变化
    const callbackCount = useCallback(() => count * 2, [count]);

    return (
        <div>
            {console.log('父组件re-render')}
            <h4>父组件的count: {count}</h4>
            <Child callbackCount={callbackCount} />
            <div>
                <button onClick={() => setCount(count + 1)}> + </button>
                <input value={val} onChange={(event) => setVal(event.target.value)} />
            </div>
        </div>
    );
}

/**
 * 这里注意了,虽然父组件传给子组件的callbackCount这个props是使用了useCallback来缓存的,
 * 但是不管你是点击了 + 按钮(count发生变化)还是在输入框输入了值(val发生变化),子组件都会被重新渲染!!
 * 为什么呢?
 * 因为useCallback仅仅是缓存了函数,并不是说count不改变的时候子组件就不会重新渲染了。只要是父组件发生变化了,子组件就会跟着被重新渲染。
 *
 * 那么useCallback的意义呢?那就看下面子组件的useEffect代码处。
 *
 */
function Child(props) {
    // 下面有两种创建初始 state的当时,第一种props.callbackCount() 每次渲染都会被调用,这就是所谓的创建对象很昂贵
    // 第二种props.callbackCount() 只会被调用一次,这就是所谓的惰性创建对象
    // const [count, setCount] = useState(props.callbackCount());
    const [count, setCount] = useState(() => props.callbackCount());

    // 根据上面的父组件的函数缓存我们知道只有父组件count改变的时候,这里面useEffect才会被执行。
    // 如果是改变父组件的val,这里的useEffect不会被执行。
    useEffect(() => {
        console.log('callback函数变化了');
        setCount(props.callbackCount());
    }, [props.callbackCount]);

    return (
        <div>
            {console.log('子组件re-render')}
           子组件的count: {count}
        </div>
    );
}

2、useCallback + memo实现真正的子组件不重新渲染

import React, { useState, memo, useCallback } from 'react';
import { Input } from 'antd';

export default function Parent() {
    console.log('父组件渲染');
    const [inputTxt, setInputTxt] = useState('');
    // 不使用useCallback,那么函数每次在调用的时候都会发生变化
    // const handleChange = (e) => {
    //     setInputTxt(e.target.value);
    // };

    // 使用useCallback来缓存函数,因为不需要依赖项,所以直接写个空数组,意思就是函数执行一次后就缓存,之后不会改变了
    const handleChange = useCallback((e) => {
        setInputTxt(e.target.value);
    }, []);

    return (
        <div className="parent-container">
            <div className="des">
                这是父组件,父组件会传递给子组件一个方法,子组件在输入框输入内容的时候会调用这个方法。
                父组件同步显示在子组件输入框输入的内容,但是因为使用了callBack这个Hook,所以子组件你不会重复渲染
            </div>
            <div className="show">
                子组件输入框输入的内容:
                {inputTxt}
            </div>
            <Child onChange={handleChange} />
        </div>
    );
}

/*
*  注意这里如果不使用memo,子组件还是会重复渲染。因为我们知道,父组件重新渲染了子组件就肯定会跟着被重新渲染,除非进行控制。
*
*  useCallback只是对函数进行了缓存。那么在子组件看来父组件传过来的onChange这个props是不变的,
*  所以我们使用memo进行浅比较props的时候就不会发生变化,所以子组件就不会重新渲染。
*/
const Child = memo((props) => {
    console.log('子组件渲染...');
    return (
        <div className="child-container mt10">
            <div className="des mb10">
                这是子组件,
                在子组件输入框中输入内容,父组件同步显示子组件输入的内容,但是子组件使用memo
                + 父组件使用useCallback之后子组件不会一直重复渲染
            </div>
            <Input placeholder="input" onChange={props.onChange} />
        </div>
    );
});

阅读文章推荐:点击这里

6、避免更改你正用于 props 或 state 的值

1、所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
2、避免更改你正用于state 的值。

import React, { useState } from 'react';
import { Button } from 'antd';

const Child = () => {
    const [words, setWords] = useState(['winne']);

    const handleClick = () => {
        // // 这部分代码很糟,因为更改了正用于 state 的值
        // words.push('winne');
        // setWords(words);

        // 下面两种方法解决
        // setWords(words.concat(['winne']));
        setWords([...words, 'winne']);
    };

    return (
        <div>
            <p>words: {words.join(',')}</p>
            <Button onClick={handleClick}>点击添加</Button>
        </div>
    );
};

export default Child;
7、合理拆分组件

微服务的核心思想是:以更轻、更小的粒度来纵向拆分应用,各个小应用能够独立选择技术、发展、部署。我们在开发组件的过程中也能用到类似的思想。试想当一个整个页面只有一个组件时,无论哪处改动都会触发整个页面的重新渲染。在对组件进行拆分之后,render 的粒度更加精细,性能也能得到一定的提升。


参考资料:
https://www.jianshu.com/p/01719e37d1f3
https://blog.csdn.net/weixin_38080573/article/details/104908371
https://blog.csdn.net/hl971115/article/details/104150794
https://blog.csdn.net/sinat_17775997/article/details/94453167
https://blog.csdn.net/hjc256/article/details/102587037

上一篇:SharePoint 2010管理中心服务器提示“需要升级”


下一篇:[目标检测]SSD原理