State Hooks - useState
语法
让我们从一个最简单的计数器案例来介绍useState
的基本使用
useState
是一个 React Hook,它允许你向组件添加一个状态变量。
其语法如下:
const [state, setState] = useState(initialState)
参数
initialState
:state 初始化的值。它可以是任何类型的值(包括函数)- 如果initialState是一个函数,则它将被视为 初始化函数。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态
const [count, setCount] = useState(() => 0)
- 如果initialState是一个函数,则它将被视为 初始化函数。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态
返回值
useState
返回一个由两个值组成的数组:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
渲染流程
通过上述案例,我们可以深入了解到:每当用户点击按钮时,会触发 setCount 函数的调用,这进而引发了 UI 的更新。然而,这一过程背后的关键逻辑是什么呢?
这个问题实际上揭示了 React 的核心概念之一 据驱动视图的原则,即 v = f(s)
:
v
: 视图(View),即用户界面(UI)f
: 渲染函数(Render Function),即你的组件s
: 状态变量(State Variables)
让我们回到计数器案例中,将 Counter
组件中返回的 JSX
替换为 React.createElement,这样做的目的是为了更直观地看到每次点击按钮时,React 如何根据状态变化生成新的 React Element 并更新 UI。
请注意,每次点击按钮时,控制台中的打印信息将展示出 React 如何根据状态的变化(通过 setCount 函数)来生成新的 React Element,从而实现 UI 的更新
不难发现,React Element 就是一个普通的 JavaScript 对象,它被 React 用来描述一部分用户界面(UI)。
Mount
当应用启动时,会触发初次渲染,React会从根组件开始遍历执行你的函数组件, 当执行到 Counter
时生成一个 React Element,该元素代表了Counter
的初始状态
{type: 'button',key: null,ref: null,props: {children: '0',},}
<button>0</button>
Trigger
当用户再次点击按钮时,setCount() 函数被触发,导致 count 从 0 更新为 1。这个状态的变化会促使 React 再次进行渲染(re-render)。
Render
这个阶段 React 会重新调用你的函数组件(re-render),以便生成一个新的 React Element,这个新元素反映了组件状态的最新变化
{type: 'button',key: null,ref: null,props: {children: '1',},}
<button>1</button>
每次渲染都像是拍摄一张快照,每个组件根据其当前的 props 和 state 生成一个 React Element,这个元素描绘了该组件在当下的外观和结构,就像是一份静态的记录,捕获了组件在那个瞬间的状态。这种方式确保了组件的表现形式始终与其内部状态保持同步,为用户界面的动态更新提供了坚实的基础
Commit
现在,我们有了两张快照
<button>0</button>
<button>1</button>
React现在必须要弄清楚如何更新DOM, 使UI与第二份快照保持一致。
不知道你有没有玩过在两张图片中寻找差异的游戏?
React本质上就是在玩这种游戏,在两份快照之间寻找差异,然后提交更新到DOM。
在Counter
这个案例中,更新DOM的操作应类似于
button.innerText = '1'
我们可以用下面这个可视化组件来描述React的渲染流程
Mount
在首次渲染组件时,由于没有先前的快照可供对比,React会从零开始构建所有必需的DOM节点,并将这些节点直接插入到网页中。
总结
- React渲染流程份为三个阶段 Trigger、Render、Commit
- 所谓Render,其实就是React在调用你的组件函数
- State可以理解为具体某次Render的UI快照
异步更新
阅读下面代码,点击按钮后比对控制台输出和页面
你会发现,
首次渲染时,我们将count
初始化为0。然后,当我们点击按钮时,调用setCount(count + 1)
将其增加1,即1。那么它不应该打印1吗,为什么是0呢?而且后续每次点击后,Console中打印的 count
值,都比页面上的值小1。
const [count, setCount] = React.useState(0) // 初始为0const handleClick = () => {setCount(count + 1) // 增加1console.log('count:', count) // ❓此时打印为什么是0 而不是1呢}
其实,当我们调用 setCount(count + 1)
时,我们实际上是在向 React 发出一个信号,表明我们希望修改某个状态变量的值(请求使用+1后的count值重新渲染)。然而,React 并不会立即执行这个修改操作;相反,它会等待当前正在进行的操作(例如处理用户点击事件)完成后,再进行状态的更新和组件的重新渲染。
到目前为止,我们需要知道的是:
React状态更新是异步的, 在具体的某一次Render过程中state是不会发生变化的,set函数只会影响下一次useState返回的结果
下面我们再用一个案例来证明上面的结论。
在这个案例中,我们通过两个按钮+1
和+2
来增加count的值,其中+2
点击时会调用两次setCount(count + 1)
。但当我们点击+2
时发现,其效果和+1
一样,每次点击只会让count的值增加1。
按照之前的结论,我们可以这样理解
const [count, setCount] = React.useState(0) // 初始为0const inscrementTwo = () => {setCount(count + 1) // 此时count为0, 我们请求React使用1作为新值重新渲染setCount(count + 1) // 此时count❗仍为0, 我们再请求React使用1作为新值重新渲染}
我们发送了两次完全相同的请求,那结果下次渲染时Count值是1而不是2。
那有什么办法能实现+2的效果吗? 自然是有的。
基于前值更新状态
为了解决上面的问题,你可以向 setCount 传递一个 更新函数,而不是下一个状态:
const inscrementTwo = () => {setCount(count + 1)setCount(count + 1)}
const inscrementTwo = () => {setCount(prevCount => prevCount + 1)setCount(prevCount => prevCount + 1)}
通过setState(prevState => nextState)
这种方式,每次调用set函数时都能拿到上一次的state。
更新复杂对象
你可以将对象和数组放入状态中。在 React 中,状态被认为是只读的,因此 你应该替换它而不是改变现有对象。