UseMemo、UseCallback、React.memo

前置问题:React中的函数式组件什么时候会重新渲染?

答:组件中的状态改变(包含Context)、父组件重新渲染导致子组件的重新渲染。

组件多次重新渲染会导致性能的下降。UseMemo、UseCallback、React.memo都利用缓存机制来提高了组件的性能。

UseMemo类似于Vue中的计算属性,将组件中计算出的值进行缓存;

UseCallback将组件中的函数进行缓存;

React.memo通过监听props的值是否改变来控制子组件是否重新渲染;

UseMemo

官方解释

代码逻辑解释:

使用useState钩子创建了一个userInfo状态,包含name和gender属性。

函数formatGender 用来根据用户的gender属性来将其格式化为中文。

handleClick 方法用来改变userInfo中的name属性。

gender 是根据函数formatGender 生成的一个值。

不使用UseMemo

当点击两个按钮,不管是修改name还是修改gender,组件中的状态都发生改变了,所以组件要重新渲染。组件的重新渲染导致formatGender 函数也要重新执行,可以看到函数中的打印语句。

如果是修改gender导致组件的重新渲染,那么formatGender 函数重新执行,这无可厚非。

但是如果是修改name导致组件的重新渲染,那么formatGender 函数就不应该重新执行,因为就算它执行了,计算出来的结果与组件重新渲染之前的计算结果还是相同的,这就白白浪费了性能。如果这个计算过程很昂贵,那么性能浪费的更多。

使用UseMemo

上述的情况可以使用UseMemo这个钩子进行优化,缓存函数上一次的计算结果。只有当特定的状态发生变化时,才会重新进行重新计算,提高了性能。

javascript 复制代码
import { useState, useMemo } from "react";
export const EgOfUseMemo = () => {
  const [userInfo, setUserInfo] = useState({
    name: "nobody",
    gender: "male",
  });

  const formatGender = (gender) => {
    console.log("调用了翻译性别的方法");
    return gender === "male" ? "男" : "女";
  };
  const handleClick = () => {
    setUserInfo({
      ...userInfo,
      name: "lvxiaobu",
    });
  };

  const updateSex = () => {
    setUserInfo({
      ...userInfo,
      gender: userInfo.gender + 'aaa',
    });
  };
  // 优化前,不使用useMemo的情况下,修改其他属性,也会重新调用formatGender方法,浪费计算资源
  // 每次点击按钮,都会调用一次formatGender方法
  // const gender = formatGender(userInfo.gender);

  // 优化后,修改其他属性,不会重新调用formatGender方法,性能提升
  // 每次点击按钮,都不会调用formatGender方法
  const gender = useMemo(() => {
    return formatGender(userInfo.gender);
  }, [userInfo.gender]);

  return (
    <div>
      姓名: {userInfo.name} -- 性别: {gender} <br />
      <button onClick={handleClick}>修改名字</button>
      <button onClick={updateSex}>修改性别</button>
    </div>
  );
};

UseCallback

官方解释

不使用UseCallback

为方便调试,请在入口文件中关闭React中的严格模式

代码解释:

使用useState钩子生成两个状态count和name,并定义两个更新状态的函数updateCount 和updateName 。

为了监听Fn函数是否是新生成的,使用useRef钩子缓存Fn函数的上一次引用;使用useEffect钩子监听Fn函数的变化,并比较Fn函数两次的引用地址是否相同。

当组件挂载时,useEffect钩子将Fn函数的引用缓存了起来。

当通过按钮来改变count或name时,组件中的状态发生改变,进行重新渲染。触发useEffect钩子,打印出预料中的结果(Fn的新引用和旧引用不相等,因为重新渲染的时候生成的是一个新函数,虽然新函数和旧函数的逻辑是一样的,但是在内存中的地址不一样)。

javascript 复制代码
import React from "react";
import { useCallback, useState, useEffect, useRef } from "react";
import Mock from "mockjs";

const EgOfUseCallback = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("zhangsan");
  const previousCacheFnRef = useRef(null);
  const updateCount = () => {
    setCount(count + 1);
  };
  const updateName = () => {
    setName(Mock.mock("@name"));
  };
  const Fn = () => {
    console.log("count变化了 ", count);
  };
  
  useEffect(() => {
    if (previousCacheFnRef.current !== null) {
      console.log("Fn 引用发生变化");
      console.log(
        "新引用和旧引用是否相等:",
        previousCacheFnRef.current === cacheFn
      );
    }
    previousCacheFnRef.current = cacheFn;
  }, [Fn]);
  return (
    <div>
      <p>count : {count}</p>
      <button onClick={updateCount}>updateCount</button>
      <p>name : {name}</p>
      <button onClick={updateName}>updateName</button>
      <br />
      <button onClick={cacheFn}>cacheFn</button>
    </div>
  );
});
export default EgOfUseCallback;
使用UseCallback

&emsp使用了useCallback之后,只有组件中特定的状态发生变化时,组件才会在重新渲染的时候生成一个新函数,否则就返回缓存的函数。可以通过useEffect钩子中的打印语句进行验证。

javascript 复制代码
  ......
  //仅仅需要将函数体用useCallback钩子进行包裹
  const cacheFn = useCallback(() => {
    console.log("count变化了 ", count);
  }, [count]);
  ......

React.memo

官方描述

一般来说,当父组件中的状态变更之后,父组件中引用的子组件也要进行刷新。

但是如果子组件中并没有用到父组件中变更的这个状态,那么理论上子组件是不需要进行刷新的。

不使用memo

代码解释:

在父组件中定义了两个状态,count和name。仅仅传递count的状态给子组件。

但是无论在父组件中改变哪个状态,都会触发子组件的重新渲染,可以通过子组件中useEffect钩子中的打印语句加以验证。

javascript 复制代码
//App.jsx
import { useState } from "react";
import Mock from "mockjs";
function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState(Mock.mock("@name"));
  return (
    <>
      <span>APP中的name:{name}</span>
      <button
        onClick={() => {
          setName(Mock.mock("@name"));
        }}
      >
        改变name
      </button>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <Child count={count}></Child>
    </>
  );
}
export default App;
javascript 复制代码
//Child.jsx
const Child = ({ count }) => {
  useEffect(() => {
    console.log("组件重新渲染了");
  });
  console.log("refesh");
  return (
    <div>
      <p>App传递过来的count:{count}</p>
    </div>
  );
};
export default Child;
使用memo

如果使用memo,就可以让子组件选择性的进行刷新。只有父组件传递给子组件的状态发生改变了,才会重新渲染子组件。

将上述子组件中的代码用memo进行包裹:

javascript 复制代码
//Child.jsx
const Child = React.memo(({ count }) => {
  useEffect(() => {
    console.log("组件重新渲染了");
  });
  console.log("refesh");
  return (
    <div>
      <p>App传递过来的count:{count}</p>
    </div>
  );
});
Child.displayName = 'Child'
export default Child;

总结

上述两个钩子和一个API都是围绕状态 来展开的。根据依赖的状态是否变化,选择性的进行值、函数或者组件的刷新。

虽然通过缓存可以提升性能,但是使用时也会造成额外的代价,比如增加内存用量、额外监听特定的状态是否变化了等等,所以在使用之前要确定自己是否真的需要它。

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