setState 执行机制
state 是什么
普通的变量无法满足修改数据视图自动更新视图。
在 React 中,状态(State)是指组件内部用来存储数据的对象。它允许你为应用程序或特定组件添加可变性。状态是私有的并且完全受控于该组件,可以随时间变化并触发 UI 更新。
useState Hook 提供了这两个功能:
- State 变量 用于保存状态数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
import { useState } from 'react'
export default function Gallery() {
// 使用 `useState Hook` 创建了一个叫做 `index` 的状态变量
// 和一个更新该状态的函数 `setIndex`。初始值为 0。
// `useState` 返回一个数组,第一个元素是当前状态(`index`),
// 第二个元素是用来更新状态的函数(`setIndex`)。这种写法利用了 `JavaScript` 的数组解构特性。
const [index, setIndex] = useState(0)
// 定义了一个名为 `handleClick` 的函数,当按钮被点击时会调用此函数。
// 在 `handleClick` 中,调用了 `setIndex` 并传递了 `index + 1` 作为参数,
// 这会导致 `index` 状态增加 1,并触发组件重新渲染以反映新的状态值。
function handleClick() {
setIndex(index + 1)
}
return (
<>
<h2>{index}</h2>
<button onClick={handleClick}></button>
</>
)
}
如果直接修改state
的状态,如下:
let index = 0
function handleClick() {
index += 1
}
我们会发现页面不会有任何的反应。
这是因为React
并不像Vue2
中调用Object.defineProperty
数据响应式或者Vue3
调用Proxy
监听数据变化。
必须通过setState
方法来告知react
组件state
已经发生改变。
异步更新
在 React 中,状态更新本质上是异步的,这是为了优化性能和确保更好的用户体验
这种设计有几个好处:
- 性能优化:通过批量处理多个状态更新,React 可以减少不必要的重渲染次数,从而提高应用的性能。
- 避免中间状态:确保所有相关的状态更新都一起生效,可以防止在一次事件处理过程中出现不一致的中间状态。
function handleClick() {
console.log('之前 setIndex:', index) // 假设此时 index 是 0
setIndex(index + 1)
console.log('之后 setIndex:', index) // 这里仍然会输出 0,因为状态更新是异步的
}
在这个例子中,即使你调用了 setIndex
,紧接着的日志语句也会打印出旧的状态值 (0),这是因为状态更新尚未完成。如果你需要基于最新的状态执行某些逻辑,应该使用 useEffect
或者在事件处理函数中直接操作。
这里有个展示其运行原理的小例子。在这个例子中,你可能会以为点击“+3”按钮会调用 setNumber(number + 1) 三次从而使计数器递增三次。
import { useState } from 'react'
export default function Counter() {
const [number, setNumber] = useState(0)
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1)
setNumber(number + 1)
setNumber(number + 1)
}}>+3</button>
</>
)
}
注意,每次点击只会让 number
递增一次!
按钮的点击事件处理函数通知 React 要做的事情:
- setNumber(number + 1):number 是 0 所以 setNumber(0 + 1)。
- React 准备在下一次渲染时将 number 更改为 1。
- setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。
- React 准备在下一次渲染时将 number 更改为 1。
- setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。
- React 准备在下一次渲染时将 number 更改为 1。
尽管你调用了三次 setNumber(number + 1),但在 这次渲染的 事件处理函数中 number 会一直是 0,所以你会三次将 state 设置成 1。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number 等于 1 而不是 3。
试着猜猜点击这个按钮会发出什么警告:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0)
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5)
alert(number) // 这里会弹出什么
}}>+5</button>
</>
)
}
如果你使用之前替换的方法,你就能猜到这个提示框将会显示 “0”:
但如果你在这个提示框上加上一个定时器, 使得它在组件重新渲染 之后 才触发,又会怎样呢?
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5)
setTimeout(() => {
alert(number) // 这里会弹窗什么
}, 3000)
}}>+5</button>
</>
)
}
视图 number 会变成 5, alert 会弹出 0
一个 state 变量的值永远不会在一次渲染的内部发生变化,即使其事件处理函数的代码是异步的。
在下次渲染前多次更新同一个 state
万一你想在重新渲染之前读取最新的 state 怎么办?
如果你想在下次渲染之前多次更新同一个 state
,你可以像 setNumber(n => n + 1)
这样传入一个根据队列中的前一个 state
计算下一个 state
的 函数,而不是像 setNumber(number + 1)
这样传入 下一个 state
值。这是一种告诉 React
“用 state
值做某事”而不是仅仅替换它的方法。
import { useState } from 'react'
export default function Counter() {
const [number, setNumber] = useState(0)
return (
<>
{/* 点击一次按钮,页面视图会变成3 */}
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1)
setNumber(n => n + 1)
setNumber(n => n + 1)
}}>+3</button>
</>
)
}
在这里,n => n + 1
被称为 更新函数。当你将它传递给一个 state
设置函数时:
React
会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。 在下一次渲染期间,React
会遍历队列并给你更新之后的最终 state
。
如果你在替换 state 后更新 state 会发生什么
这个事件处理函数会怎么样?你认为 number 在下一次渲染中的值是什么?
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
{/* 点击button后, 这里的number会是什么 */}
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5)
setNumber(n => n + 1)
}}>增加数字</button>
</>
)
}
这是事件处理函数告诉 React 要做的事情:
- setNumber(number + 5):number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。
- setNumber(n => n + 1):n => n + 1 是一个更新函数。 React 将 该函数 添加到其队列中。
在下一次渲染期间,React 会遍历 state 队列:
React 会保存 6 为最终结果并从 useState 中返回。
总结
在 React 中,状态更新本质上是异步的,这是为了优化性能和确保更好的用户体验。然而,如果你确实需要同步地获取状态更新后的值(例如,当你需要立即基于新的状态执行某些逻辑),你可以采取以下几种方法来实现类似的效果:
- 使用回调函数形式的状态更新
当你的新状态依赖于前一个状态时,你可以传递一个更新函数给setState
。这个函数接收前一个状态作为参数,并返回一个新的状态。这不会使状态更新变成同步的,但它可以确保你总是基于最新的状态进行计算。
setNumber(n => n + 1)
- 使用 useEffect Hook 监听状态变化
useEffect 可以用来监听状态的变化并在状态更新后执行副作用。它会在组件挂载、更新或卸载时运行指定的代码块。通过这种方式,你可以“响应式”地处理状态变化,虽然这不是真正的同步,但它提供了一种方式来保证在状态更新之后执行特定逻辑。
import { useState, useEffect } from 'react'
export default function Gallery() {
const [index, setIndex] = useState(0)
useEffect(() => {
// 这个效果将在 index 状态改变后触发
console.log('Index has changed:', index);
}, [index]) // 注意这里的依赖数组
function handleClick() {
setIndex(index + 1)
}
return (
<>
<h2>{index}</h2>
<button onClick={handleClick}>Increment</button>
</>
)
}
setState 主要通过以下步骤工作:
1. 调用 setState:
当组件内部调用 setState 方法时,它实际上是在调用由 React 创建的 hook 函数(例如 useState 或者类组件中的 this.setState)。这些函数会将状态更新请求添加到一个队列中。
2. 创建更新对象:
每次调用 setState 都会创建一个更新对象(update object),这个对象包含了新状态的值(或计算新状态的函数)以及其他元数据(如优先级等)。
3. 挂起更新:
更新对象被添加到该组件的状态队列(fiber node queue)中。React 使用 Fiber 架构来管理这些更新,允许它暂停、恢复和重新排列任务以优化性能。
4. 批量处理更新:
如果有多个 setState 调用几乎同时发生(例如,在同一个事件处理器中),React 会尝试将它们合并成一次更新,以减少不必要的渲染次数。
5. 触发重渲染:
最终,当所有更新都被处理完毕后,React 会根据新的状态来协调(reconcile)并重新渲染组件树。这包括检查哪些部分发生了变化,以及仅更新那些确实改变了的DOM节点。
6. 同步更新:
对于某些需要立即执行的情况,React 提供了 flushSync API (React 17 开始引入) 来确保状态更新立即生效。