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

相关推荐
炫饭第一名2 小时前
Lottie-web 源码解析(一):从 JSON Schema 认识 Lottie 动画的本质📒
前端·javascript·css
卖火箭的小男孩2 小时前
# Flutter Provider 状态管理精讲(Vue 开发者视角)
前端
前端_yu小白2 小时前
react常用优化手段
前端·javascript·react.js·性能优化·usecallback·usememo
攀登的牵牛花2 小时前
前端向架构突围系列 - 框架设计(六):解析接口职责的单一与隔离
前端·架构
开开心心_Every2 小时前
离线黑白照片上色工具:操作简单效果逼真
java·服务器·前端·学习·edge·c#·powerpoint
Mintopia2 小时前
🌌 信任是否会成为未来的货币?
前端·人工智能·aigc
fqbqrr2 小时前
2601C++,模块导出分类
前端·c++
倚栏听风雨2 小时前
vscode 运用 ts 代码需要准备什么
前端
韩曙亮2 小时前
【Web APIs】浏览器本地存储 ① ( window.sessionStorage 本地存储 | window.localStorage 本地存储 )
服务器·前端·javascript·本地存储·localstorage·sessionstorage·web apis