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