我们不知道 React 状态钩子是如何工作的
本文是关于:
- 状态何时更新
- 更新队列和惰性计算
- 批处理
- useState 与 useReducer
- 性能优化
- 急切计算状态更新
- 浅层渲染和救援
- 更新程序功能会一直运行吗?
状态何时更新?
看看这段代码:
const MyComp = () => {
const [counter, setCounter] = useState(0);
onClick = () => setCounter(prev => prev + 1);
return <button onClick={onClick}>Click me</button>
}
你能想象点击按钮并调用 setCounter 之后会发生什么吗?是这样的吗?
- React 调用更新函数 (prev => prev + 1)
- 更新钩子的状态(= 1)
- 重新渲染组件
- 渲染函数调用 useState 并获取更新的状态 (== 1)
如果你真的这么想,那你就错了。我以前也犯过同样的错误,直到我做了一些实验,并查看了 hooks 的源代码。
更新队列和惰性计算
事实证明,每个钩子都有一个更新队列。当你调用该setState
函数时,React 不会立即调用更新函数,而是将其保存在队列中,并安排重新渲染。
在此钩子之后,可能会有更多更新,针对此钩子、其他钩子,甚至树中其他组件的钩子。
可能有一个 Redux 操作会导致树中许多不同位置的更新。所有这些更新都已排队 - 尚未计算任何内容。
最后,React 自上而下重新渲染所有计划渲染的组件。但状态更新仍未执行。
仅当 useState 在渲染函数期间实际运行时,React 才会运行队列中的每个操作,更新最终状态并将其返回。
这就是所谓的lazy computation
——React 只有在真正需要的时候才会计算新的状态。
总而言之,发生的事情是这样的(简化):
- React 队列是这个钩子的一个动作(我们的更新函数)
- 安排组件重新渲染
- 当渲染真正运行时(稍后会详细介绍):
- 渲染运行 useState 调用
- 只有这样,在 useState 期间,React 才会遍历更新队列并调用每个操作,并将最终结果保存在钩子的状态中(在我们的例子中 - 它将是 1)
- useState 返回 1
批处理
那么,React 什么时候会说:“好了,队列更新和渲染调度已经完成了,现在让我开始工作吧”?它又是如何知道更新已经完成的呢?
每当有事件处理程序(onClick、onKeyPress 等)时,React 都会在批处理中运行提供的回调。
批处理是同步的,它会运行回调,然后刷新所有已安排的渲染:
const MyComp = () => {
const [counter, setCounter] = useState(0);
onClick = () => { // batch starts
setCounter(prev => prev + 1); // schedule render
setCounter(prev => prev + 1); // schedule render
} // only here the render will run
return <button onClick={onClick}>Click me</button>
}
如果回调函数中包含异步代码怎么办?这些代码将在批处理之外运行。在这种情况下,React 将立即启动渲染阶段,而不是将其安排到稍后执行:
const MyComp = () => {
const [counter, setCounter] = useState(0);
onClick = async () => {
await fetch(...); // batch already finished
setCounter(prev => prev + 1); // render immediately
setCounter(prev => prev + 1); // render immediately
}
return <button onClick={onClick}>Click me</button>
}
State 是 Reducer
我之前提到过“React 会运行队列中的每个 action”。谁说过 action 是什么?
事实证明,其底层useState
只是useReducer
以下内容basicStateReducer
:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
因此,我们的setCounter
功能实际上是dispatch
,并且您发送给它的任何内容(值或更新程序功能)都是动作。
我们所说的一切useState
对于都是有效的useReducer
,因为它们在幕后都使用相同的机制。
性能优化
你可能会想——如果 React 在渲染时计算了新的状态,那么如果状态没有改变,它怎么能跳过渲染呢?这是一个先有鸡还是先有蛋的问题。
这个答案分为两部分。
该过程实际上还有另一个步骤。在某些情况下,当 React 知道可以避免重新渲染时,它会立即计算该操作。这意味着它会立即运行该操作,检查结果是否与之前的状态不同,如果相同,则不会安排重新渲染。
第二种情况是,React 无法立即调用 action,但在渲染过程中,React 发现没有任何变化,所有状态钩子都返回了相同的结果。React 团队在他们的文档中对此进行了很好的解释:
退出状态更新
如果你将 State Hook 更新为与当前状态相同的值,React 将退出状态更新,不会渲染子元素或触发任何效果。(React 使用 Object.is 比较算法。)请注意,React 在退出之前可能仍需要再次渲染该特定组件。这无需担心,因为 React 不会不必要地深入渲染树。如果您在渲染过程中进行了昂贵的计算,可以使用 useMemo 进行优化。
https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
简而言之,如果没有任何变化,React 可能会运行渲染函数并在那里停止,并且不会真正重新渲染组件及其子组件。
更新程序功能会一直运行吗?
答案是否定的。例如,如果发生任何异常,导致渲染函数无法运行,或者在中途停止,我们就不会执行该useState
调用,也不会运行更新队列。
另一种选择是,在下一个渲染阶段,我们的组件会被卸载(例如,如果父组件内部的某些标志发生变化)。这意味着渲染函数甚至不会运行,更不用说useState
表达式了。
学到了新东西?发现什么错误了吗?
在下面的评论部分让我知道
文章来源:https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8