React useCreation hook 思考与解析
本文主要对于掘金小册《玩转 React Hooks》的思考
引文
以下内容摘自《玩转 React Hooks》
useCreation:强化 useMemo 和 useRef,用法与 useMemo 一样,一般用于性能优化。
useCreation 如何增强:
- useMemo 的第一个参数 fn,会缓存对应的值,那么这个值就有可能拿不到最新的值,而 useCreation 拿到的值永远都是最新值;
- useRef 在创建复杂常量的时候,会出现潜在的性能隐患(如:实例化
new Subject),但 useCreation 可以有效地避免。
思考
useMemo无法获取最新值得情况
useMemo是一个React Hook,它可以在渲染期间优化组件的性能。useMemo的作用是在函数组件中缓存计算结果,只有在依赖项发生变化时,才会重新计算结果。如果依赖项没有发生变化,useMemo将会直接返回上一次的计算结果,不会重新计算。
所以,在以下情况下,useMemo可能无法返回最新的值:
- 当依赖项没有发生变化时,
useMemo会返回上一次缓存的值。因此,如果你需要访问最新的状态,请确保组件的依赖项已经发生了变化。 - 如果你使用了异步操作或副作用操作,并且这些操作不是在
useMemo中管理的,那么useMemo的值可能无法保证最新。 - 如果你修改了组件依赖项之外的其他部分,即使依赖项发生了变化,
useMemo也可能无法返回最新的值。
异步操作的影响
下面是一个简单的例子,模拟了一个通过网络请求获取数据的情况:
import React, { useState, useMemo } from 'react';
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('data');
}, 5000);
});
}
function ExpensiveComponent() {
const [count, setCount] = useState(0);
const expensiveValue = useMemo(async () => {
const data = await fetchData();
return `${data}-${count}`;
}, [count]);
console.log('expensiveValue:', expensiveValue);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
export default ExpensiveComponent;
async关键字声明的函数,返回数据的类型是Promise,那么我们不能直接将值显示在页面上,所以直接将结果打印出来
在这个例子中,我们模拟了一个通过网络请求获取数据的情况,useMemo的依赖项是count。当你点击“Increase Count”按钮时,计数器会增加一,这意味着useMemo的依赖项发生了变化,并且useMemo应该返回一个新的值。
你会发现
useMemo返回的值Promise的状态都是pending,无法获取到最新的值
这是因为,网络请求是在useMemo内部之外发生的,useMemo无法控制,因此,即使依赖项发生了变化,useMemo也会返回上一次缓存的值。
import React, { useState, useMemo } from 'react';
function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
const expensiveValue = useMemo(() => {
if (!data) return null;
return `${data}-${count}`;
}, [data, count]);
useMemo(async () => {
const result = await fetchData();
setData(result);
}, []);
console.log('expensiveValue:', expensiveValue);
return (
<div>
<p>{expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
解析
源码
import { useRef, type DependencyList } from "react";
/**
* 判断依赖项是否相同
* @param oldDeps 旧的依赖项
* @param deps 新的依赖项
* @returns
*/
const depsAreSame = (
oldDeps: DependencyList,
deps: DependencyList
) => {
if (oldDeps === deps) return true
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
const useCreation = <T,>(fn: () => T, deps: DependencyList) => {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
})
/**
* 触发更新的两种情况
* 1. 未初始化
* 2. 依赖项的改变
*/
if (!current.initialized || !depsAreSame(current.deps, deps)) {
current.deps = deps
current.initialized = true
current.obj = fn()
console.log(current.obj)
}
return current.obj as T
}
export default useCreation
export default ExpensiveComponent;
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('data');
}, 1000);
});
}
在这个新的代码中,我们把网络请求的部分放到单独的useMemo中,并且在依赖项中传入一个空数组,表示这部分代码只需要执行一次,这其实效果和useEffect(() => {....},[])相同。并且在useMemo的依赖项中添加data状态,useMemo就可以管理这部分代码了,并且可以保证expensiveValue始终是最新的值。
总结
假设你的组件通过
useMemo计算一个比较耗时的值,并且这个值需要通过网络请求来获取,这时候就需要用到异步操作。但是,如果你的网络请求不是在useMemo内部进行管理,useMemo返回的值可能就无法保证最新。
解析
import { useRef, type DependencyList } from "react";
/**
* 判断依赖项是否相同
* @param oldDeps 旧的依赖项
* @param deps 新的依赖项
* @returns
*/
const depsAreSame = (
oldDeps: DependencyList,
deps: DependencyList
) => {
if (oldDeps === deps) return true
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
const useCreation = <T,>(fn: () => T, deps: DependencyList) => {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
})
/**
* 触发更新的两种情况
* 1. 未初始化
* 2. 依赖项的改变
*/
if (!current.initialized || !depsAreSame(current.deps, deps)) {
current.deps = deps
current.initialized = true
current.obj = fn()
}
return current.obj as T
}
export default useCreation
useRef存储hook的deps(依赖数组)、initialized(是否初始化)、obj(存储的变量),这个参数的作用是应对首次保存值,之后判断是否保存,根据 deps 判断即可。