一定要掌握的React性能优化 memo,useMemo,useCallback

前言

在现代Web开发中,React已经成为构建用户界面的首选库之一。随着应用变得越来越复杂,性能优化成为确保良好用户体验的关键因素。

React组件默认会在状态或属性变化时重新渲染,即使某些子组件并不依赖于这些变化。这可能导致额外的计算开销,极大的消耗程序的运行速度,这显然是不可接受的,本文我们将探究这些新跟那个优化问题。

memo

memo官称为React.memo,官方介绍也就一句话:memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。

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

正常情况,我们只需要在父组件外部编写

const MemoizedChildComponent = memo(ChildComponent);

然后便可以在父组件内部使用MemoizedChildComponent组件即可了,但是切忌这里是不能在父组件内部以表达式的形式使用,因为memo的本质就是HOC组件(高阶组件),React的函数式编程是不允许在一个组件内部再次定义一个组件的。

这里我们要讲就讲点不一样改动,这里第二个参数的回调函数其实也是非常有趣的:arePropsEqual

typescript 复制代码
import React, { useState, useEffect, memo } from 'react';

// 定义子组件的类型接口
interface PropsType {
  counter: number;
}

// 子组件
function ChildComponent({ counter }: PropsType) {
  console.log('子组件被渲染');
  return (
    <div>
      {counter}
    </div>
  );
}

// 使用 memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent);

// 父组件
export default () => {
  const [counter, setCounter] = useState<number>(0);

  // 处理计数器增加
  const handleCounter = () => {
    setCounter(prevState => prevState + 1);
  };
  console.log('父组件被渲染');
  return (
    <div>
      <h1>父组件</h1>
      <p>counter: {counter}</p>
      <button onClick={handleCounter}>点击+1</button>
      <MemoizedChildComponent counter={counter} />
    </div>
  );
};

上面我们正常使用是没问题的,子组件接受了父组件响应式,当该数据发生改变,子组件都会渲染。

但是当我们显示的指定第二个参数的返回值时,我们便可以用这个来控制子组件的响应式更新:

javascript 复制代码
// 使用 memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent,(oldProps,newProps)=>true);

运行程序再来看看打印结果:

可以看到子组件在最开始便失去了父组件的响应式的更新,因此在业务开发中,某些子组件在做有限次任务时,内部便可指定第二个参数的判断,结合闭包使用便可达到效果。

useMemo

useMemo用于缓存计算结果,有点Vue中的computed的味道了。官方介绍:useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

ini 复制代码
const cachedValue = useMemo(calculateValue, dependencies)
参数
  • calculateValue:要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。React 将会在首次渲染时调用该函数;在之后的渲染中,如果 dependencies 没有发生变化,React 将直接返回相同值。否则,将会再次调用 calculateValue 并返回最新结果,然后缓存该结果以便下次重复使用。
  • dependencies:所有在 calculateValue 函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。如果你在代码检查工具中 配置了 React,它将会确保每一个响应式数据都被正确地定义为依赖项。依赖项数组的长度必须是固定的并且必须写成 [dep1, dep2, dep3] 这种形式。React 使用 Object.is 将每个依赖项与其之前的值进行比较。
返回值

在初次渲染时,useMemo 返回不带参数调用 calculateValue 的结果。

在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue,并返回最新结果。

这里我们便用一个示例搞清楚useMemo的使用:

javascript 复制代码
import React, { useMemo, useState } from 'react';

export default function App() {
  const [arr, setArr] = useState(new Array(10).fill(0));
  const [expensiveValue, setExpensiveValue] = useState(null);

  // 使用 useMemo 来记忆计算结果
  const memoizedValue = useMemo(() => {
    console.log('memoizedValue 被调起使用');
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.random();
    }
    return result;
  }, [arr.length]); // 仅当 arr.length 变化时重新计算

  // 模拟一个昂贵的计算
  const handleCalculate = () => {
    setExpensiveValue(memoizedValue);
  };

  return (
    <div>
      <p>Array.length: {arr.length}</p>
      <p>Value: {expensiveValue}</p>
      <button onClick={() => setArr(prev => [...prev, 0])}>数组长度+1</button>
      <button onClick={handleCalculate}>计算结果</button>
    </div>
  );
}

如果是没有useMemo的情况下,每次的组件更新这个memoizedValue每次都将被调起执行,但是当我们使用useMemo之后,只有我们的依赖项改变之后,需要使用到它的计算结果之后才会取调起函数来使用。

useCallback

useCallback用于函数。啥?缓存啥函数?,官文一句话:useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

ini 复制代码
const cachedFn = 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是非常有用的,一起先看一个小demo:

javascript 复制代码
import React, { useState, useCallback,memo } from 'react';

const Child = ({ onClick }) => {
  console.log('子组件被渲染');
  return (
    <button onClick={onClick}>
     按钮
    </button>
  );
};
// 使用 memo 包裹子组件,以防止不必要的重新渲染
const ChildComponent=memo(Child);

export default ()=>{
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(prevCount => prevCount + 1);
    console.log('按钮被点击,函数触发');
  }
  
  return (
    <div>
      <h1>父组件</h1>
      <p>{count}</p>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

这个需求是十分正常的对吧,给子组件传入数据修改方法,但是每一次子组件还是会因为函数的传入而每次刷新,即使你使用memo来包裹。因为前面说了:memo 进行的是浅比较,即它只会检查对象或函数的引用是否相同。如果每次渲染时传递的新函数是一个新的函数实例,即使函数的内容没有改变,memo 也会认为 props 发生了变化。

这时我们就需要缓存function来减少渲染了,修改handleClick函数:

ini 复制代码
  // 使用 useCallback 记忆 handleClick 函数
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
    console.log('按钮被点击,函数触发');
  }, []); // 空数组表示没有依赖项,函数永远不会改变

结语

memo,useMemo,useCallback便是我们前端react最主要的三大性能优化神器了。无论你是面试,还是真实的应用开发都是必须要熟练运用的,简单使用并不困难,进阶的话就需要深入研究了。

相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom9 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github