React Hooks常量的useMemo和useState区别

18

使用React hooks定义计算出的(初始化)常量可以通过两种看似功能相同的方式完成。我不想深入探讨这个用例,但可以说有时可以从初始props或状态中派生出不会预期改变的常量值(考虑路由数据、绑定调度等)。

首先,使用useState

const [calculatedConstant] = useState(calculateConstantFactory);

其次,useMemo

const calculatedConstant = useMemo(calculateConstantFactory, []);

这两者看起来在功能上是等价的,但如果不阅读源代码,我不确定哪个在性能或其他考虑因素方面更好。

有人对此进行了研究吗?你会使用哪个,为什么?

另外,我知道有些人会对“状态可以被视为常量”的假设感到不适。我不确定怎么告诉你。但即使没有状态,我可能还想在一个没有任何状态的组件中定义一个常量,例如创建一个不变的 JSX 块。

我可以将其定义在组件之外,但是这样会消耗内存,即使该组件在应用程序中没有实例化也是如此。为了解决这个问题,我需要创建一个记忆函数,然后手动释放内部记忆状态。这对于 hooks 来说是一件很麻烦的事情。

编辑:添加了在此讨论中讨论的方法的示例。 https://codesandbox.io/s/cranky-platform-2b15l


使用 ref 怎么样? - Alvaro
工厂模式的重点在于它仅在第一次渲染时运行。之后,工厂不会被调用。这允许常量可以任意复杂,但逻辑只需运行一次。Refs 不接受像 useStateuseMemo 这样的工厂函数。 - bfricka
也许我误解了,但我的意思是像你在这里使用useState一样使用ref const calculatedConstant = useRef(calculateConstantFactory);。如果有帮助,只是提出第三个选项。 - Alvaro
这只是你示例中工厂的引用。意思是:calculatedConstant.current === calculateConstantFactory。你仍然需要调用工厂来获取你的值。在我的示例中,工厂只被调用一次。 - bfricka
这是人为的,因为至少调用了函数来绑定名称,但你明白的。https://codesandbox.io/s/cranky-platform-2b15l - bfricka
1个回答

11

使用 useMemo 可以优化性能,但并不能保证语义正确

也就是说,从语义上讲,useMemo 不是正确的方法,您正在错误地使用它。因此,尽管它现在可以正常工作,但您的使用方式可能不正确,将来可能会导致不可预测的行为。

只有当您不希望在计算值时阻止渲染时,才应该选择 useState

如果组件的第一次渲染中不需要该值,则可以使用 useRefuseEffect 结合使用:

const calculatedConstant = useRef(null);

useEffect(() => {
  calculatedConstant.current = calculateConstantFactory()
}, [])

// use the value in calcaulatedConstant.current

这与在componentDidMount中初始化实例字段相同。并且在运行工厂函数时不会阻止布局/绘画。从性能上来说,我怀疑任何基准测试都不会显示出显着的差异。

问题在于,在初始化引用之后,组件不会更新以反映此值(这是引用的整个目的)。

如果您绝对需要在组件的第一次渲染中使用该值,则可以这样做:

const calculatedConstant = useRef(null);

if (!calculatedConstant.current) {
  calculatedConstant.current = calculateConstantFactory();
}
// use the value in calculatedConstant.current;

这个会阻止您的组件在设置值之前进行渲染。

如果您不希望阻止渲染,您需要将useStateuseEffect一起使用:

const [calculated, setCalculated] = useState();

useEffect(() => {
  setCalculated(calculateConstantFactory())
}, [])

// use the value in calculated

如果需要组件重新渲染,请使用 state。如果不需要重新渲染,则使用 ref。


这是一个很好的答案。感谢您提供useMemo注意事项的链接。不使用状态管理非状态数据也是有道理的。我可以利用useRefuseEffect的组合构建钩子。对于您的第二个示例,我认为您想要放置if (!calculatedConstant.current)。感谢您宝贵的意见。 - bfricka
@bfricka 实际上我很抱歉,我忘记了一件非常重要的事情。你可能需要使用第二种方法和if语句,否则组件不会更新以反映初始化的ref值(calculatedConstant.current将为空,直到组件因其他原因更新,例如prop或state更改)。 我已更新响应以反映这一点,并修复了你提到的检查。 - Raicuparta
1
是的,我已经到那里了。基本上我使用了两个引用。`export const useConstant = <T>(factory: () => T): T => { const initRef = useRef(false); const valueRef = useRef<T>(); if (!initRef.current) { initRef.current = true; valueRef.current = factory(); } return valueRef.current as T; };` - bfricka
1
我再次更新了它,以解释useState在这里实际上可以有一些有效的用途,具体取决于您想要实现什么。 - Raicuparta
function async 是错误的;应该是 async function。此外,你不能使用 useState(calculateConstantFactory) -- 这只会将 calculated 设置为从该函数返回的 Promise 而不是它实际解析的值。 - Herohtar
很好的回答。另一个因素是useMemo实际上做了比你需要的更多,即进行记忆化处理。如果你有一次初始化操作并且对渲染方法安全,那么useState正是你所需要的。如果钩子API参考手册中提到这种情况会更好。 - Francis Upton IV

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接