usePrevious 和类似的面向时间的钩子的问题
本周,Theo 制作了一个关于 React hooks 的非直观行为的视频,特别探讨了名为“usePrevious”的 hook 的概念,用于在当前重新渲染之前保留上一次重新渲染的值版本。这是一种使用新旧状态进行逻辑处理的方法。
如果您想了解如何实现它的想法,请观看视频,在这篇文章中,我们的想法是探索像“usePrevious”这样的钩子的概念方面。
反应性表达式
如您所见,React 中没有这样的原始钩子。在钩子出现之前,在基于类的时代,我们有一个名为 componentDidUpdate 的生命周期方法,我们将所有先前的状态和 props 作为参数获取其中,为什么他们没有使用钩子保留这种行为?
如果您正在阅读这一系列文章,可能会有点重复,但在这种情况下,我们需要讨论范式转变。
对于类,当某些状态更新时,您无法自动重新计算派生值。如果您使用某些特定的 props 和状态来计算某些新值,则需要自行验证其中某些值是否已更改。
这样,解决方案就是在所有更新中调用一个回调,并将之前的值发送给用户。应用程序代码检查差异并使用新结果更新计算状态。这是基于类的组件的直接性,您可以完全控制数据流,并且无需手动控制计算。
这里我们来讨论反应性表达式。
您无需检查并进行更改,只需编写一个表达式(计算公式)即可。此计算需要使用当前状态版本执行,而无需访问上一个版本。
想象一个公式:
a = b + c b = 10 c = 20 a = 10 + 20 a = 30
如果我使用这个表达式 100 万次,将 b 传递为 10,将 c 传递为 20,我会得到相同的结果。这是一个纯计算。React 执行相同的原理。所有派生计算都应该是纯的。
React 以重新渲染的方式工作。每次循环都会生成 UI 描述,并根据当前渲染和下一个渲染之间的差异,将更改提交到 DOM。每次渲染都与上一次或下一次渲染完全分开。
UI = fn(state)
因此,对于每个不同的状态版本,我们都会得到不同的 UI 版本。如果我们在这里添加以前的值,这会变得非常混乱。因为现在它不仅取决于状态,还取决于以前的状态。我可以有多个源,也许还有更复杂的表达式来处理这些源,从而得到不一致且不可预测的 UI,而不是只有一个源、一个表达式和一个结果。
每次渲染都会根据之前的状态做出不同的行为。而且由于 `usePrevious` 的一些可能实现依赖于 React 中的时间顺序,这变得更加危险。
借助并发功能,React 可以在不警告渲染的情况下停止渲染,以优先处理其他操作。依赖 `useEffect` 和 ref 可能会让您保留“上一次”渲染的陈旧版本,而该版本甚至是真正的上一次渲染。更多混乱需要推理。
记忆化
像这样思考
a = b + (c - d)
其中一部分具有优先级,需要先计算,我们用 javascript 代码来思考一下:
const cdResult = c - d; const a = b + cdResult;
所以现在我们有两个可以单独计算的独立表达式,并且是完全纯净的。但是,如果 b 的值变化很大,并且 cdResult 的计算成本很高,我们该如何解决呢?记忆!
const cdResult = React.useMemo(() => c - d, [c, d]); const a = b + cdResult;
现在,如果 c 或 d 发生变化,则会重新计算 `cdResult`。
其实不是。例如:
// render 1 // c = 30; d = 20 const cdResult = React.useMemo(() => c - d, [c, d]); // = 10
假设我们处于渲染编号 1 中。c 的值为 30,d 的值为 20,因此结果为 10。但是当我对其进行记忆时,React 会跟踪我在数组上添加的依赖项。如果其中一些发生变化,它会重新计算。
// render 2 // c = 30; d = 20 const cdResult = React.useMemo(() => c - d, [c, d]); // = 10
但它们并没有改变。如果我再次调用此表达式,将 c 设为 30,将 d 设为 20,我将得到相同的结果 10。即使我处于渲染编号 2 中并且其他变量已更改,但我在此计算中使用的依赖项并未更改。
我可以在每次渲染时再次计算,这是 React 的默认行为,但我可以选择跳过不必要的重新计算,这将返回相同的值,所以我保留了它。我们保持了纯度,也保持了渲染之间的分离
先前状态
但是,有一个好地方可以对先前的状态、用户操作进行逻辑处理。当然,在调用回调的那一刻,那就是当前状态。但是,如果您有一些需要根据某种逻辑进行更改的状态,那么这里就是好地方。
当然,它可能有非常特殊的情况,也许你需要一个钩子,比如“usePrevious”,但要注意它可能导致的不一致性,并尝试添加保证以避免应用程序出现错误。
更重要的是,如果可能的话,避免这种情况。