• React(16.13.1)中useEffect依赖改变时的渲染顺序以及性能提升学习


    目录

    一、 单个tsx文件依赖改变时渲染顺序

    1、useEffect简单情况

    这是最简单的情况;每次组件render的时候,最先要明白的是useEffect第二个参数,一个依赖项的数组;分以下 3 * 2 种情况:

     

    组件首次渲染

    组件非首次渲染,在有state改变时渲染

    useEffect无第二个参数

    执行

    执行

    useEffect第二个参数是空数组

    执行

    不执行

    useEffect第二个参数是非空数组

    执行

    改变的state在数组内 执行 否则 不执行 ;

    注:改变采用浅比较,此处不深究比较原理;不建议传入引用类型作为依赖项,如果在useEffect中修改了引用类型,则会引发无限渲染的问题

    注:useEffect中的回调函数不只是在组件销毁前调用,而是在每一轮的下一次render前也会调用,所以回调函数的执行逻辑同上表;

     

    const Index1 = () => {
        const [test, setTest] = useState(1)
        console.log(1)
    
        useEffect(() => {
            console.log(4)
        })
    
        useEffect(() => {
            console.log(5)
        }, [])
    
        useEffect(() => {
            console.log(6)
        }, [test])
    
        console.log(2)
    
        return (
            <>
                {console.log(3)}
                <h1>这里是Index1</h1>
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    首先就是不论什么时候渲染;均是从上往下执行代码;(此处重点讨论useEffect,不讨论同步、异步、宏任务和微任务;有兴趣可以阅读),

    直到执行完return内的代码;最后回过头从上往下执行 每个useEffect内的代码;我们称之为一个渲染循环;打印顺序为:1、2、3、4、5、6;可以分批理解为1、2、3为挂载前执行,4、5、6为挂载后执行;

    点击Button改变状态值,从上到下会依然打印 1、2、3、4、6;

    需要注意的是,如果是在执行代码过程当中触发的state改变;则需要先执行完当前渲染循环;然后执行下一个渲染循环;以此类推;

    const Index2 = () => {
        const [test, setTest] = useState(1)
        console.log(1)
    
        useEffect(() => {
            console.log(4)
            return () => {
                console.log(11)
            }
        })
    
        useEffect(() => {
            console.log(5)
            const timer = setInterval(() => {
                console.log('每秒打印一次')
            }, 1000)
            return () => {
                console.log(12)
                clearInterval(timer)
            }
        }, [])
    
        useEffect(() => {
            console.log(6)
            return () => {
                console.log(13)
            }
        }, [test])
    
        console.log(2)
    
        return (
            <>
                {console.log(3)}
                <h1>这里是Index2</h1>
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    2、useEffect回调执行逻辑

    在Index1的基础上,我们加上useEffect的回调函数,其实回调不仅仅是在组件willunmount的时候执行那么简单;

    当然对于上面的例子,组件卸载时,11、12、13会依次打印;

    但其实 点击Button时;会依次打印1、2、3、11、13、4、6;也就是对于11、13 这2个回调而言,所在的useEffect再次render前会执行上次回调;12所在useEffect不会影响,故不会打印;

    12所在的useEffect只会在组件卸载时候 执行回调;所以我们平时把清除定时器、清除onscroll等放在依赖为[]的useEffect内;

    关于回调,其实在卸载组件之后不会立马执行,而是在即将挂载的组件挂载前和挂载后的中间执行;(这点不重要;感兴趣的读者可以下去探索)

    二、 tsx文件嵌套tsx文件时渲染顺序

    const Index = () => {
        const [test, setTest] = useState(1)
        console.log(1)
    
        useEffect(() => {
            console.log(4)
        })
    
        useEffect(() => {
            console.log(5)
        }, [])
    
        useEffect(() => {
            console.log(6)
        }, [test])
    
        console.log(2)
    
        return (
            <>
                <Child1 />
                {console.log(3)}
                <h1>这里是Index</h1>
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
                <hr />
            </>
        )
    }

     

    const Child1 = () => {
        const [test, setTest] = useState(1)
        console.log('child1', 1)
    
    
        useEffect(() => {
            console.log('child1', 4)
        })
        
          useEffect(() => {
            console.log('child1', 5)
        }, [])
    
        useEffect(() => {
            console.log('child1', 6)
        }, [test])
    
        console.log('child1', 2)
    
        return (
            <>
                {console.log('child1', 3)}
                <h1>这里是Child1</h1>
            </>  
        )
    }

    1、初次渲染

    执行顺序其实和第一大点(单个jsx(tsx)文件依赖改变时渲染顺序)一致;每执行一次渲染的时候,从组件最上方执行到最下方,依然是按照从上到下,执行完之后,再按照上表的6种情况执行useEffect内的代码;

    这里 需要注意的是;若嵌套子组件;则Child属于当前组件的 return内容;执行完之后,才会执行当前组件的useEffect;也就是先打印2才会打印1;

    所以上面代码打印顺序为:1、2、3、child1 1、child1 2、child1 3、child1 4、child1 5、child1 6、4、5、6;这里比较特殊的是:console.log(3)在<Child1 />下面;但是却先打印了3;

    这里仅仅是嵌套一个Child1;读者可以脑补一个Child2和Child1平级;打印则是:1、2、3、child1 1、child1 2、child1 3、child2 1、child2 2、child2 3、child1 4、child1 5、child1 6、child2 4、child2 5、child2 6、4、5、6;

    可以看出来要先把2个子组件的挂载前执行完毕才去执行useEffect;

    2、state改变时

    若是子组件state改变,和父组件无关,毋需讨论;

    若是父组件state改变,打印顺序为:1、2、3、child1 1、child1 2、child1 3、child1 4、4、6;这里我们得到 在渲染过程中,只是把依赖数组内无关的过滤掉了;其余没有什么特别之处;

    三、 tsx文件嵌套tsx文件时提升性能

    1、利用React.memo()

    从上面的分析学习我们不难发现;组件嵌套,初次渲染和父组件state改变时;无论子组件有没有变动,只要父组件render,所有的子组件都会重新render!

    初次渲染无法提升性能;但是父组件state改变时,我们可以做一些事情;这就请出我们的另一个主要讨论对象:React.memo(); 简单代码如下:

     

    const Index = () => {
        return (
            <>
                <Child1  props1 = {'props1'} />
                <Child2  props2 = {'props2'} />
            </>
        )
    }
    

     

     

    const Child1 = (props) => {
        return (
            <>
                {console.log('child1')}
                <h1>这里是Child1</h1>
                <hr />
            </>
        )
    }
    export default Child1

     

    const Child2 = (props) => {
        return (
            <>
                {console.log('child2')}
                <h1>这里是Child2</h1>
                <hr />
            </>
        )
    }
    
    export default React.memo(Child2)

    如上 Child1为参照;当index中state更新,新一轮render后,打印了child1,child2并未打印;证明memo起了作用;

    index每一次render时候,child1无论如何都会每次render;这时候我们把重点放在child2上;我们将<Child2 /> 的props2变一下;变成非定值;而是和state相关的;Child1和Child2不变;如下:

    const Index = () => {
    		const [test, setTest] = useState(1)
        return (
            <>
                <Child1  props1 = {'props1'} />
                <Child2  props2={test} />
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    这时候我们发现 改变state时,child1和child2都打印了;证明2个子组件都刷新了;所以我们有了个初步的结论:当memo不传入第二个参数时,父组件render时;当props不改变;则该子组件不会render;props有改变时,则该子组件会render!

    但是到这还没完;不禁猜想若 child2的props是个父组件本轮render不相关的state会怎么样;而非一个相关state;如下:

    const Index = () => {
    		const [test, setTest] = useState(1)
        const [definiteV, setDefiniteV] = useState(1)
        return (
            <>
                <Child1  props1 = {'props1'} />
                <Child2  props2={definiteV} />
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    我们注意到此时;即便test改变;definiteV不改变(即便是definiteV是一个引用类型;但我们平时开发中,不建议state使用引用类型);Child2也不会重新render;所以我们可以有了提升性能第一个小措施:当子组件不论是否接收props,都用React.memo()包裹,会在无关props变化改变时减少子组件刷新次数;提升性能;

    提到基本类型和引用类型;虽然在不相关state中是无差别的;但作为一个普通变量就不一样了;我们看一下如下代码:

    const Index = () => {
    		const [test, setTest] = useState(1)
        return (
            <>
                <Child1  props1 = {'props1'} />
                <Child2  props2={[1,2,3]} />
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    我们把child2的props2换成数组;再次改变test;发现child1和child2都打印了;证明child1和child2都重新render;我们猜想是因为数组的引用地址不一样,造成每次diff不同(memo的diff有点类似useEffect依赖的浅比较);

    这显然是不符合我们预期的;我们就引出有一个hook;叫:useMemo;我们试着这样写:

    2、结合使用useMemo和useCallback

    const Index = () => {
    		const [test, setTest] = useState(1)
         const memoValue = useMemo(() => [1, 2, 3], [])
        return (
            <>
                <Child1  props1 = {'props1'} />
                <Child2  props2={memoValue} />
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    我们发现再次改变test;child2不打印了;证明useMemo起了效果;所以我们又有了一个提升性能的小措施:给子组件传的引用类型props最好可以结合使用useMemo以减少子组件不必要刷新,当然子组件需要结合使用React.memo;

    还有一种特殊类型;也是我们工作中经常传给子组件的;那就是方法;

    const Index = () => {
    		const [test, setTest] = useState(1)
        const fn1 = () => {}
        const fn2 = useCallback(() => {},[])
        return (
            <>
                <Child1  props1 = {'props1'} />
                <Child2  props2={fn1} />
                <Button
                    onClick={() => {
                        setTest(v => v + 1)
                    }}
                >
                    改变状态值
                </Button>
            </>
        )
    }

    当改变test,父组件一轮render中;props2分别传入fn1和fn2的时候,传入fn1时候child2会重新render,传入fn2的时候则不会;useCallback和useMemo类似;区别是useMemo接收的是函数返回的值;而useCallback返回的即第一个参数,一个方法;

    还有一个点值得我们注意:useMemo和useCallback都有第二个参数;一个依赖项数组;类似于useEffect的第二个参数;我们在文章前面分析了不少;此处不再赘述;

    3、提一下React.memo()的第二个参数

    当我们把Child2的代码稍作改动;加上memo()的第二个参数;如下:

    const Child2 = (props) => {
        return (
            <>
                {console.log('child2')}
                <h1>这里是Child2</h1>
                <hr />
            </>
        )
    }
    
    export default React.memo(Child2, (prev, next) => {
        return false
    })

    在Child2每一轮render中; prev就是上一轮的props,next为下一轮即将接收的props;类似于React类组件的shouldComponentUpdate()类似;

    我们可以人为的对比前后props,进行对组件(Child2)刷新控制;个人认为React.memo第二个参数实用性不大;经过上面的分析学习不难发现React已经帮我们做的很好了;根本不需要我们自己去控制;仅在特别情况下我们可以利用,大部分情况用不到;

     

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    数字资产交易所记录
    How to decode input data from a contract transaction without ABI?
    【收藏】ETH以太坊各个环境的公共的RPC服务!!!
    Solidity知识点集 — 溢出和下溢
    docker run 与docker start的区别
    子网掩码计算192.168.1.0/24 24 / 11
    Solidity-让合约地址 接受ETH的转账充值的 三种方式
    echarts的散点图
    debug.js中的length的错误
    26个工具类
  • 原文地址:https://www.cnblogs.com/XieYFwin/p/16135306.html
Copyright © 2020-2023  润新知