useRef存在的潜在性能问题

最近在学习ahooks源码时,在useCreation时,发现官方文档写了这样一句话

第一眼后,我心想:useRef 不是只会执行一次吗?并且不会随组件生命周期变化吗?然后根据官方文档的例子试了一下,发现 useRef 在存储复杂数据类型时(比如对象),确实有性能问题

tsx 复制代码
import { Button } from "antd";
import { useRef, useState } from "react";

class Subject {
  constructor() {
    console.log("===constructor===");
    this.data = Math.random();
  }
  data: number;
}

const RefQuestion = function RefQuestion() {
  const { current } = useRef(new Subject());
  const [, setFlag] = useState({});
  return (
    <div className="pt-10">
      <p>{current.data}</p>
      <Button
        onClick={() => {
          setFlag({});
        }}
      >
        Rerender
      </Button>
    </div>
  );
};

export default RefQuestion;

在上面的代码中,组件初始化时,会执行一次 new Subject(),控制台会打印一次 ===constructor===

当我点击按钮触发组件重新渲染时,虽然渲染的值没有变,但是控制台依旧打印了===constructor===

也就是说明:组件重新渲染时,虽然React内部判断后,返回了首次渲染时的值,但是在每次组件渲染时,都会执行 new Subject() 实例化过程,即使每次实例化后,都丢弃了实例化对象,而重复实例化对象就是一种无效的内存开销,即性能存在隐患

因此我重新看了下React文档对于 useRef 的说明:

可见 React 官方就已经对这种情况进行了说明,并且也给出了解决方案,思想与单例模式一致

tsx 复制代码
function Video() {  
    const playerRef = useRef(null);  
    if (playerRef.current === null) {  
        playerRef.current = new VideoPlayer();  
    }  
    // ...

然后我们回过头来,可以看看 useCreation 是怎么做的

tsx 复制代码
import type { DependencyList } from 'react';
import { useRef } from 'react';
import depsAreSame from '../utils/depsAreSame';

const useCreation = <T>(factory: () => T, deps: DependencyList) => {
  const { current } = useRef({
    deps,
    obj: undefined as T,
    initialized: false,
  });
  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = factory();
    current.initialized = true;
  }
  return current.obj;
};

export default useCreation;
  • 如果是首次渲染(initialized === false)或者 依赖项发生了改变(!depsAreSame(current.deps, deps)),则执行 factory()函数创建值
  • 如果不满足条件,直接返回之前的值(此时不会走 factory())

这样就保证了通过 useCreation 创建的值一定是 memosized,依赖改变前是保证不会重新计算的

我们将 useCreation 代替 useRef 试试

tsx 复制代码
import { useCreation } from "ahooks";
import { Button } from "antd";
import { useState } from "react";

class Subject {
  constructor() {
    console.log("===constrcutor=====");
    this.data = Math.random();
  }
  data: number;
}

const RefQuestion = function RefQuestion() {
  const [flag, setFlag] = useState({});
  const foo = useCreation(() => new Subject(), []);
  return (
    <div className="pt-10">
      <p>{foo.data}</p>
      <Button
        onClick={() => {
          setFlag({});
        }}
      >
        Rerender
      </Button>
    </div>
  );
};

export default RefQuestion;

点击按钮,组件重新渲染,不会执行 new Subject() 的实例化过程

那如果依赖发生改变,每次也只会重新计算一次

tsx 复制代码
import { useCreation } from "ahooks";
import { Button } from "antd";
import { useState } from "react";

class Subject {
  constructor() {
    console.log("===constrcutor=====");
    this.data = Math.random();
  }
  data: number;
}

const RefQuestion = function RefQuestion() {
  const [flag, setFlag] = useState({});
  const foo = useCreation(() => new Subject(), [flag]);
  return (
    <div className="pt-10">
      <p>{foo.data}</p>
      <Button
        onClick={() => {
          setFlag({});
        }}
      >
        Rerender
      </Button>
    </div>
  );
};

export default RefQuestion;

如图,我点击了八次按钮,依赖改变了八次

所以,useRef并不是只会执行一次,而是每次组件渲染都执行,只不过如果之前有结果就返回之前的结果而已。

相关推荐
栖凤11 分钟前
Java 的 try-catch 在 Agent 里失效了,我用了 5 个模式才兜住
java·前端·python
粥里有勺糖11 分钟前
视野修炼第136期 | 前端小恐龙"Deno"被收购
前端·github·ai编程
修炼的dance16 分钟前
页面塞了13.88 MB字体,导出的PDF只有71 KB
前端
qq_3691736317 分钟前
如何将 AI 生成的 HTML 网页发布成在线链接?不用自己搭建服务器
前端·人工智能·html·效率工具·html 发布
风花一世月24 分钟前
⚡ 我把一座含氢能源园区搬进了浏览器(五类能流实时算守恒,在线直接玩)
前端·人工智能
羲云1 小时前
Claude 越界事件之后:Agent 权限设计为什么不能只靠提示词
前端
行者全栈架构师1 小时前
【鸿蒙心迹】鸿蒙网络请求架构实战——@ohos.net.http 到 Axios 封装、拦截器与统一错误处理(HarmonyOS 7.x)
前端·算法·架构
全栈Agent 小李1 小时前
【无标题】
前端·后端·agent·ai编程·全栈·cursor·mcp
羲云1 小时前
别让 AI 凭记忆做调研:给 WorkBuddy 接上可核验的实时搜索
前端
deli0072 小时前
连接池到底配多大?QPS 和 RT 算一遍,再跑 1 万次请求实测超时率
前端