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并不是只会执行一次,而是每次组件渲染都执行,只不过如果之前有结果就返回之前的结果而已。

相关推荐
子兮曰6 小时前
async/await高级模式:async迭代器、错误边界与并发控制
前端·javascript·github
恋猫de小郭6 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
GIS之路8 小时前
ArcGIS Pro 中的 Notebooks 入门
前端
IT_陈寒10 小时前
React状态管理终极对决:Redux vs Context API谁更胜一筹?
前端·人工智能·后端
Kagol11 小时前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉11 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau11 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生11 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼11 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范
明君8799711 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter