React memo机制-React.memo, useCallback,useMemo,浅对比到底咋回事?

921 阅读5分钟

React的更新默认是全等对比,类组件中相同的state也会引起重新渲染,函数式组件中则不会。 直接修改state,之后使用修改过的state更新,因为地址相同,也不会引起重新渲染。

首先看一下React浅对比的源码,React有很多地方应用到浅对比,这是一个比较重要的对比函数。 image.png

重要逻辑:只会对比第一层,地址一样算没有改变。

Object.is

这个函数几乎可以和===等同,唯一的区别是认为-0和+0不是同一个值,以及对NAN的判断。

MDN的解释: image.png

React.memo

class组件实现了PureComponent高阶组件,对当前组件进行缓存,每次props改变,对新旧props进行浅对比来决定是否复用缓存。 React.memo就是PureComponent的函数式组件缓存实现。React.memo有两个参数,第一个是要缓存的组件,第二个是自定义对比逻辑,使用场景是我们完全了解这个组件的更新逻辑才写,比如说props比较复杂,而我们十分确定只有其中的几个属性修改才重新渲染,就可以编写自定义对比逻辑,减少对比消耗。

React.memo没必要尽量不要使用,因为会多很多没有用的计算过程,心智负担,循环依赖,以及容易忽视导致性能差的核心问题,代码可读性差。

官网对memo的建议翻译成中文:

如果你的应用程序像这个网站一样,而且大多数交互都是粗略的(比如替换一个页面或整个部分),备忘录通常是不必要的。另一方面,如果你的应用程序更像一个绘图编辑器,而且大多数交互都是颗粒状的(比如移动形状),那么你可能会发现备忘录非常有用。 使用useMemo进行优化只在少数情况下有价值:

你放在useMemo中的计算明显很慢,而且它的依赖关系很少改变。 你把它作为一个道具传递给一个用memo包装的组件。如果值没有变化,你想跳过重新渲染的过程。Memoization让你的组件只在依赖关系不一样的时候才重新渲染。 你所传递的值后来被用作某个Hook的依赖项。例如,也许另一个useMemo的计算值依赖于它。或者,也许你是依赖于useEffect的这个值。 在其他情况下,用useMemo包装一个计算值并没有什么好处。这样做也没有明显的坏处,所以有些团队选择不考虑个别情况,而尽可能地进行memo化。这种方法的缺点是,代码的可读性变差。另外,并不是所有的备忘化都是有效的:一个 "永远是新的 "的单值就足以破坏整个组件的备忘化。

在实践中,你可以通过遵循一些原则使大量的记忆化成为不必要的:

当一个组件在视觉上包裹其他组件时,让它接受JSX作为子女。这样,当包装组件更新它自己的状态时,React知道它的孩子不需要重新渲染。 倾向于本地状态,不要把状态提升到超过必要的范围。例如,不要保留像表单这样的瞬时状态,以及一个项目是否在你的树顶或全局状态库中被悬停。 保持你的渲染逻辑的纯净。如果重新渲染一个组件会导致一个问题或产生一些明显的视觉伪影,那就是你的组件中的一个bug! 修复这个bug,而不是增加memoization。 避免不必要的更新状态的效果。React应用程序中的大多数性能问题都是由源于Effects的更新链引起的,这些更新链导致你的组件不断地渲染。 试着从你的Effects中移除不必要的依赖性。例如,与其说是备忘录化,不如说是将一些对象或函数移到Effect里面或组件外面,这样做往往更简单。 如果一个特定的交互仍然感觉滞后,使用React开发者工具剖析器,看看哪些组件会从备忘化中受益最多,并在需要时添加备忘化。这些原则使你的组件更容易调试和理解,所以在任何情况下遵循这些原则都是好的。从长远来看,我们正在研究自动进行细化的备忘化,以一劳永逸地解决这个问题。

useCallback和useMemo

useCallback,useMemo这两个HOOKS逻辑接近,思路基本相同。对依赖项进行浅对比决定是否使用缓存,如果使用缓存的话,地址就会相同。地址相同那么配合React.memo走浅比较的第一个比较逻辑Object.is() 会直接使用缓存。

不使用这两个API,每一次渲染页面都会创建新的变量/函数,地址不相同,React.memo不生效。

遇到卡顿,我们可以按照以下步骤优化代码逻辑:

  1. 排查代码BUG
  2. 使用React开发者工具找出具体哪个函数性能不好
  3. 检查,减少减少Effect不必要的依赖项(因为大部分性能不好都是因为Effect的依赖关系)
  4. 不得不使用memo,靠React开发者工具对比提升幅度,确定优化有效
注意不可变数据!!
React.memo和useCallbak,useMemo结合,单写useCallbak,useMemo,虽然使用了缓存,但是对用到该props的子组件还是重新渲染!!单写React.memo,传过来的props还是新的方法/变量,也不能达到优化性能的目的。