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已经帮我们做的很好了;根本不需要我们自己去控制;仅在特别情况下我们可以利用,大部分情况用不到;

 

 

 

 

 

 

 

 

 

 

 

posted @ 2022-04-12 15:16  谢勇飞~  阅读(1197)  评论(0编辑  收藏  举报