我们不知道 React 状态钩子是如何工作的

2025-06-04

我们不知道 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
PREV
VSCode:使用 Black 自动格式化 Python
NEXT
优化 Django ORM 使用 ORM,Luke Hasta la vista,模型 向我展示 SQL(第一部分) 向我展示 SQL(第二部分) 向我展示 SQL(第三部分) 一个工具栏控制它们全部 选择并预取所有相关项 小心模型的实例化 根据 ID 进行过滤让世界运转 只遵从你的内心内容 注释并继续 批量粉碎!呃,创建 我们想让你变得庞大 会让你出汗(现在每个人都使用 Raw Sql) 穿上丽兹