一定要掌握的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最主要的三大性能优化神器了。无论你是面试,还是真实的应用开发都是必须要熟练运用的,简单使用并不困难,进阶的话就需要深入研究了。

相关推荐
彭世瑜6 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4047 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish8 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five9 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序9 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54110 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
前端每日三省11 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
小刺猬_98511 分钟前
(超详细)数组方法 ——— splice( )
前端·javascript·typescript
渊兮兮13 分钟前
Vue3 + TypeScript +动画,实现动态登陆页面
前端·javascript·css·typescript·动画
鑫宝Code13 分钟前
【TS】TypeScript中的接口(Interface):对象类型的强大工具
前端·javascript·typescript