最近在做react
项目的迁移,从class
组件写法迁移至function
组件,不得不说hooks
的特性对整个代码逻辑复用性的提高还是很不错的。但是新手在接触hooks
的时候,如果不了解各个API
的特性的话,同样也会埋下许多神坑,后期优化费时费力。
要解决性能问题,关键在于对组件重复渲染的处理,魔鬼在细节,下面就来通过两个常见的案例来分析性能到底会毁在哪几个细节,并且渐进式地给出优化方案。
组件状态管理混乱
const ShowUp = () => {
const [tabVisible, setTabVisible] = useState(false);
const [dataSource, setDataSource] = useState([]);
const [countdown, setCountdown] = useState(0)
...
const countdownHandler = () => {
if (countdown === 10) return
setTimeout(() => {
setCountdown(countdown + 1)
countdownHandler()
}, 1000)
}
useEffect(() => {
if (tabVisible) {
countdownHandler()
}
}, [tabVisible])
return (
<Tab visible={tabVisible}>
{dataSource}
</Tab>
)
};
分析案例
这种代码风格在新手刚开始接触hooks
的时候会很常见,把组件的各个状态都用useState
进行初始化,类比class
组件就是把所有的变量都放在state
里。
这个案例中的业务逻辑是想要打开tab
的10秒后关闭,然后状态中的tabVisible
,dataSource
是和视图层有关系的,但是countdown
缺没必要绑定useState
,因为countdown
倒计时的时候,每一秒都会设置一次视图层,这样对页面的渲染就是不必要。同时,上面的代码中还存在闭包陷阱,countdownHandler
方法在调用的时候,内部的countdown
永远都是0,这个递归永远无法停止下来。
对症下药
我们先看闭包问题怎么解决,闭包问题的关键在于如何获取最新的countdown
值,这时候就可以改变下我们setState
的调用形式,改成传入回调函数的方式来更新值。
setCountdown(oldCountdown => oldCountdown + 1)
其实这样做还是没有解决问题,因为在判断countdown
是否等于10的时候,countdown
值还是被闭包缓存下来了,所以递归还是跳不出来。
这时候就要我们的useRef
登场了,useRef
可以拿来当class
组件的this
上下文来用,而且用useRef
定义的变量更新的时候并不会更新视图层,这个特性和class
组件里的this
变量相同。
const countdown = useRef(0)
const countdownHandler = () => {
if (countdown.current === 10) return
setTimeout(() => {
countdown.current += 1
countdownHandler()
}, 1000)
}
当然实际业务场景的话可能不会写countdown
,直接setTimeout
设置10秒就好了,但是针对这种countdown
类似的场景,就有更好的选择了。
Props无关渲染
const Parent = () => {
const [visible, setVisible] = useState(false);
const [level, setLevel] = useState(1);
return (
<>
<Child1 visible={visible} />
<Child2 level={level} />
</>
)
}
案例分析
分析一下上面的伪代码,假如调用setVisible
更新了visible
,Child2
组件是否会更新呢?
答案:如果Child2
内部没做优化,那么肯定会更新
但这并不是我们想要的结果,随便改个状态就导致所有子节点都更新的话,那就非常耗费性能了,不过也不用太过于担心,因为react
内部其实会有做diff
算法比较,一些处理逻辑简单的组件的渲染耗时几乎可以忽略不计。同时,react
在版本的迭代中把15的stack reconciler
改进成了16的fiber reconciler
,简称fiber
,这使得更新视图的时候,有一个调度器来决定哪些行为会优先执行,保证视图交互顺畅,而不是一直占用js主程,导致页面看起来卡死了一样。密集计算逻辑的组件的话就要思考一下怎么去优化优化。
对症下药
这种场景其实考虑一下class
组件的做法,我们会选择继承PureComponent
的方式来优化生命周期,这是最简单一种方式,如果是函数式组件的话,就要用React.memo
这个api
来优化,使用起来也很简单,在组件外层包裹一层即可,有点像HOC
。
const Child2 = React.memo((props) => {
...
return (
<div>
...
</div>
)
})
做了第一步优化后,我们可以继续对Child2
组件内部做第二步优化,比如说每次props
只有部分属性会导致大量计算,那么我们就能对这一部分的属性进行缓存,如果这部分属性不变的话,那么就会跳过计算,hooks
就有这样的API
,useMemo
和useCallback
就是用来缓存高开销的计算。
const Child2 = React.memo((props) => {
const realInfo = useMemo(() => {
return computeExpensiveValue(props.a, props.b)
}, [props.a, props.b])
return (
<div>
...
</div>
)
})
这里只会在props.a
和props.b
变化时,才进行computeExpensiveValue
函数的调用,否则,realInfo
一直会用上一次计算后缓存下来的值,这样的话,虽然props
有更新,但是也并不会完全执行全部业务逻辑。
总结
这两个场景基本能涵盖到大部分的hooks
性能优化,不过react
性能优化的话还有别的很不错的方案,之后我们有机会再来聊聊。