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