对于useCallback、useMemo、React.memo是否要大量使用的一些思考

1,309 阅读7分钟

前言

React-Hook使用了两年多了,自从使用了hook以后就再也不想写class了,哪怕是改之前的class代码,都希望用hook重构一下,hook写起来真的太爽了!但是有一问题一直都不是很确定,就是useCallback、useMemo、React.memo是否值得大量使用甚至强制使用。趁年末没啥工作,利用摸鱼时间梳理一下自己的见解

什么是useCallback、useMemo和React.memo

  • useMemo: 把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值, 主要用于将变量缓存
  • useCallbackuseMemo的语法糖,返回一个 memoized 回调函数。主要用于将函数缓存
  • React.memo:类似于PureComponent,主要是为了在父组件渲染时防止对没有状态变化的子组件进行不必要的渲染

现在团队的做法

所有组件的中的变量和函数都强制要求使用 useCallback和 useMemo。

React.memo不做强制的要求,除非遇到内部逻辑特别复杂,重复渲染会影响性能的组件,否则都可以不用。

网上收集的一些讨论

官方将useCallback、useMemo、React.memo都定义为性能优化的手段,没有强制说要使用,导致开发者刚接触这些hook时一脸懵逼,好像不用也可以,又好像用了会更好,网上资料一搜,越看越懵,感觉谁说的都对,下面梳理下关于用不用的一些网上依据,主要还是这篇帖子的一些回复。

能不用就不用

  • 业务和视图解耦,如果出现了很多的useCallback和useMemo则说明架构本身出了问题(总的来说就是React只负责视图层的东西,尽量做得很轻很轻,最好跟业务一点都不挂钩)

    • react是个view library,react持有的state应当只是「临时的视图状态」,而不是「应用状态」。使用react的state来承载应用状态,会带来业务逻辑和视图层的耦合。
    • 传回调和把函数作为依赖都是很差的代码设计
    • 在 model 层获取数据,并不与 setState 耦合,视图层自行根据自己的情况 setState 即可。获取数据和展示数据是相互分离的
  • useCallback的一些问题

    • 上下文调用顺序的问题
    • 组件卸载时获取最新 state 的问题(其实这不能算 useCallback 的坑,React 设计如此。)
    • 依赖没有写清楚时容易写出“闭包陷阱”,拿到的是旧的值
  • 满屏的useCallback和useMemo会增加开发负担

  • 使用了useCallback 就减少了子组件渲染?其实单独使用是没什么效果的,需要和memo配合使用

  • 过渡使用useCallback和useMemo会过度优化甚至是负优化

  • 必要使用的情况

    • 复杂的计算值用useMemo缓存
    • 当值作为别的 hooks 的依赖时
    • 需要作为被 memo 的子组件的 props 时

尽量使用

  • 不使用useCallback会造成每次渲染时函数的重建
  • 不使用useMemo和useCallback会造成React.memo失效
  • 不使用useCallback 则 useEffect无法依赖 回调函数
  • React.memo 类似 PureComponent 能用就用
  • useMemo 做复杂推导时必用,简单计算用了也不会错。

完全不用/无所谓

  • 不用没有任何关系,只有遇到性能问题时再局部优化

我的观点

性能方面:

使不使用useCallback和useMemo的区别不大,我们没必要去纠结缓存一个函数/变量和重复创建一个函数/变量会造成多大的性能损耗,多次渲染重建和过多缓存都不是造成性能瓶颈的原因,但是从直觉上来看,我更倾向于避免一个函数重复的创建

而且在函数组件中,除非你使用了React.memo来避免组件的重新render,否则无法保证组件被调用的时候是否会因为父组件一直重新render导致子组件的重新render,从这个角度上看,缓存函数比不停的重新创建函数会更好一点

修正: 由于使用useCallback时,函数会作为实参传给useCallback,所以无论怎样useCallback包裹的函数都是会重新创建的,只是当useCallback的依赖没有改变时返回的是缓存中的函数而已。

const onClick = useCallback(function a() {
        console.log('something')
}, [dep1, dep2])

如上面的的代码,无论dep1和dep2是否改变,function a 都会被创建(因为function a是作为实参传给useCallback的),只是dep1和dep2没有改变的话,onClick永远都是之前创建的function a的缓存

拆分组件才是优化性能的最好方式: react数据更新 是以当前组件为根节点的 整个组件树,所以 优先拆分组件 是最好的优化性能的方法,使用useCallback和useMemo是否造成负优化这个问题不值得一提,毕竟是否负优化跟这个组件会重新渲染多少次有很大的关系

必要性方面:

无论支持哪一方的观点,以下几种情况都应该使用:

  • 复杂的计算值用useMemo缓存
  • 当值作为别的 hooks 的依赖时
  • 需要作为被 memo 的子组件的 props 时

想要真正的不用useMemo和useCallback是不现实的,你不知道你的子组件是否用了React.memo,也不知道父组件是否会一直触发子组件的重新渲染(render),所以最好的方法是”假设当前组件会一直重新render,我们要去避免由于函数/变量的重新创建造成子组件重新渲染的情况”,尽量的加上useMemo和useCallback可以保证使用了React.memo的组件都起作用

还有一个观点是忘掉这两个api,等遇到性能问题时再去使用useMemo和useCallback,我不认为平时基本不使用这两个hook的人在遇到性能优化问题时能快速的进行组件的性能提升重构

还有一个“尽量使用useMemo、useCallback”的优点是它会让你不自觉的以hook的思维来编写你的代码,让你真正的拥抱hook,不容易出错,遇到问题时也能以hook的思维来思考,去分析每次执行是否拿到了最新值,是什么导致代码不按预期来执行的。

总之,只有当你很熟悉hook是什么时才能更好的去判断是否要使用它,而最好的熟悉方法就是不断地去使用它,不断地去纠正自己理解误区,等你真正的掌握时才能说出那句“看情况用吧”,而不是简单的一句官方不推荐,对性能也没有提升就让自己放弃了对hook熟悉的机会

团队代码规范方面

说到心智负担,有的人觉得全部都加useCallback和useMemo是一种负担,有的人觉得该不该加是一种负担,我的看法是尽量的去规范团队的行为,全部都加上不会出什么大问题,并且在使用的过程中会更加的了解这两个hook的作用,我们不能确保每个人都能一上手就完全了解这两个hook的作用,强制加上这两个hook不仅可以减少review代码压力,也可以给新加入的伙伴学习和试错的机会

稳定性方面

强制加上useCallback和useMemo除了多写两行代码并不会影响代码的稳定,如果因为依赖少加导致取到旧的值,我觉得是开发者的问题(而且我们有编辑器帮助我们检查是否有漏掉依赖,这不应该是个问题)

总结

最终我们团队还是保持现有的方案,所有组件的中的变量和函数都强制要求使用 useCallback和 useMemo。React.memo不做强制的要求(其实我也是建议能用就用),除非遇到内部逻辑特别复杂,重复渲染会影响性能的组件,否则都可以不用。