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

相关推荐
harrain1 小时前
什么!vue3.4开始,v-model不能用在prop上
前端·javascript·vue.js
fanruitian6 小时前
uniapp android开发 测试板本与发行版本
前端·javascript·uni-app
rayufo6 小时前
【工具】列出指定文件夹下所有的目录和文件
开发语言·前端·python
RANCE_atttackkk6 小时前
[Java]实现使用邮箱找回密码的功能
java·开发语言·前端·spring boot·intellij-idea·idea
2501_944525548 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 支出分析页面
android·开发语言·前端·javascript·flutter
李白你好8 小时前
Burp Suite插件用于自动检测Web应用程序中的未授权访问漏洞
前端
刘一说10 小时前
Vue 组件不必要的重新渲染问题解析:为什么子组件总在“无故”刷新?
前端·javascript·vue.js
徐同保10 小时前
React useRef 完全指南:在异步回调中访问最新的 props/state引言
前端·javascript·react.js
刘一说11 小时前
Vue 导航守卫未生效问题解析:为什么路由守卫不执行或逻辑失效?
前端·javascript·vue.js
一周七喜h12 小时前
在Vue3和TypeScripts中使用pinia
前端·javascript·vue.js