3个React关于memo,useCallback的应用和开发及面试常发生的场景

205 阅读7分钟

1. memo

memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
    • memo(Component, arePropsEqual?)
    • Component:要进行记忆化的组件。memo 不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和 forwardRef 组件。
    • 可选参数arePropsEqual:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop
    • 返回值-memo 返回一个新的 React 组件。它的行为与提供给 memo 的组件相同,只是当它的父组件重新渲染时 React 不会总是重新渲染它,除非它的 props 发生了变化。

2. useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

const cachedFn = useCallback(fn, dependencies)
    • useCallback(fn, dependencies)
    • fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。React 将会在初次渲染而非调用时返回该函数。当进行下一次渲染时,如果 dependencies 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。
    • dependencies:有关是否更新 fn 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。React 使用 Object.is 比较每一个依赖和它的之前的值。
    • **返回值-**在初次渲染时,useCallback 返回你已经传入的 fn 函数在之后的渲染中, 如果依赖没有改变,useCallback 返回上一次渲染中缓存的 fn 函数;否则返回这一次渲染传入的 fn
  • 注意
    • useCallback 是一个 Hook,所以应该在 组件的顶层 或自定义 Hook 中调用。你不应在循环或者条件语句中调用它。如果你需要这样做,请新建一个组件,并将 state 移入其中。
    • 除非有特定的理由,React 将不会丢弃已缓存的函数。例如,在开发中,当编辑组件文件时,React 会丢弃缓存。在生产和开发环境中,如果你的组件在初次挂载中暂停,React 将会丢弃缓存。在未来,React 可能会增加更多利用了丢弃缓存机制的特性。例如,如果 React 未来内置了对虚拟列表的支持,那么在滚动超出虚拟化表视口的项目时,抛弃缓存是有意义的。如果你依赖 useCallback 作为一个性能优化途径,那么这些对你会有帮助。否则请考虑使用 state 变量ref

3. 在 React 组件中更新嵌套状态

3.1. 问题:

您在 React 组件中有一个深度嵌套的状态对象,它表示用户信息,包括用户的地址。您的任务是在单击按钮时更新地址对象中的城市属性。挑战在于确保在状态中仅更新城市值,而不直接改变原始状态。单击“更新城市”按钮后,UI 应反映新的城市值。

具体任务:单击“更新城市”按钮时,将城市值更新为“旧金山”,并确保此更改立即反映在 UI 中。

3.2. 解决方案:

为了正确更新城市属性而不改变原始状态,您应该使用扩展运算符创建需要更新的状态的每个级别的浅表副本:

此方法可确保状态对象的每个级别都被不可变地复制和修改。单击“更新城市”按钮时,updateCity 函数会创建一个新的状态对象,其中只有地址对象内的城市属性会更新为“旧金山”。

使用扩展运算符 (...) 有助于通过创建每个嵌套对象的新副本而不是直接修改它们来保持不变性。这确保了 React 的状态管理有效运行,并且 UI 正确反映了更新的城市值。

4. 防止子组件不必要的重新渲染

4.1. 问题:

你有一个父组件,它将 props 传递给子组件。每当父组件重新渲染时,子组件也会重新渲染,即使它的 props 没有改变。挑战在于优化组件结构,以便当父组件的状态更新但子组件的 props 保持不变时,子组件不会不必要地重新渲染。

4.2. 场景:

考虑一个维护两个状态的父组件:计数和数据。计数状态经常更新,而数据状态保持不变。父组件渲染一个 ChildComponent,将数据作为 prop 传递。

目标是防止在计数状态改变但数据不变时 ChildComponent 重新渲染。

4.3. 解决方案:

为了防止在计数状态改变但数据保持不变时 ChildComponent 重新渲染,你可以使用 React.memo 来记忆 ChildComponent。

  • React.memo:这个高阶组件会记住 ChildComponent 的渲染输出。如果传递给 ChildComponent 的数据 prop 没有改变,则当父组件由于其他状态(如计数)的更改而重新渲染时,React.memo 将阻止 ChildComponent 重新渲染。
  • 工作原理:单击“增加计数”按钮时,ParentComponent 中的计数状态将更新。通常,这会导致 ParentComponent 和 ChildComponent 都重新渲染。但是,使用 React.memo,ChildComponent 仅在数据 prop 更改时才会重新渲染。由于数据保持不变,控制台日志“ChildComponent 已渲染”仅出现一次,表明 ChildComponent 不会不必要地重新渲染。

5. 使用 useCallback 防止不必要的重新渲染

5.1. 问题:

您有一个父组件,它将事件处理程序函数作为 prop 传递给子组件。每当父组件重新渲染时,子组件也会重新渲染,因为函数 prop 被重新创建。挑战在于防止子组件在父组件状态改变时不必要地重新渲染。

5.2. 场景:

在父组件中,有一个计数状态,每次单击“增加计数”按钮时,计数状态都会增加。handleClick 函数作为 prop 传递给 ChildComponent。

即使 ChildComponent 用 React.memo 包装,它仍然会在父组件重新渲染时重新渲染,因为 handleClick 函数每次都是一个新的引用。目标是使用 useCallback 来防止这种不必要的重新渲染。

5.3. 解决方案:

要阻止 ChildComponent 每次 ParentComponent 重新渲染时重新渲染,请使用 useCallback 来记忆 handleClick 函数。

  • useCallback:记忆 handleClick,这样它就不会在每次渲染时重新创建。这可确保 ChildComponent 仅在实际 props 发生变化时重新渲染。
  • React.memo:除非其 props 发生变化,否则防止 ChildComponent 重新渲染。

通过使用 useCallback,handleClick 函数在渲染过程中保留相同的引用,即使父状态(计数)发生变化,也可以防止 ChildComponent 不必要的重新渲染。

6. 结论

要掌握 React,重要的是不仅要了解如何创建组件,还要了解如何使它们高效。本文中的问题——关于更新嵌套状态、停止不必要的重新渲染以及使用 useCallback——专注于解决您在实际 React 项目中可能遇到的常见问题。