React Hooks实战沉淀:性能优化

5,515 阅读4分钟

最近在做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秒后关闭,然后状态中的tabVisibledataSource是和视图层有关系的,但是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更新了visibleChild2组件是否会更新呢?

答案:如果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就有这样的APIuseMemouseCallback就是用来缓存高开销的计算。

文档

const Child2 = React.memo((props) => {
  
  const realInfo = useMemo(() => {
  	return computeExpensiveValue(props.a, props.b)
  }, [props.a, props.b])
  
	return (
  	<div>
    	...
    </div>
  )
})

这里只会在props.aprops.b变化时,才进行computeExpensiveValue函数的调用,否则,realInfo一直会用上一次计算后缓存下来的值,这样的话,虽然props有更新,但是也并不会完全执行全部业务逻辑。

总结

这两个场景基本能涵盖到大部分的hooks性能优化,不过react性能优化的话还有别的很不错的方案,之后我们有机会再来聊聊。