详细介绍React的useState

1,340 阅读8分钟

useState 是React的一个基本Hook,它允许函数组件拥有自己的状态。在React的早期版本中,只有类组件能够有状态,但是自从Hooks在React 16.8中引入后,函数组件也可以使用状态了。useState提供了一种简单的方式来在组件渲染之间保持和更新状态。

使用useState的基本方法

useState接收一个参数,即状态的初始值,然后返回一个数组,其中包含两个元素:当前的状态值和一个更新该状态的函数。这是一个基本的使用示例:

import React, { useState } from 'react';

function Counter() {
  // 声明一个新的状态变量,我们将其称为“count”
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

在上面的例子中,useState(0)声明了一个新的状态变量count,其初始值为0。setCount是一个允许我们更新count的函数。当用户点击按钮时,我们调用setCount来增加count的值。

useState的工作原理

当你调用useState时,React会在内部为当前组件保持一个状态,并在组件的每次渲染之间保持这个状态。当你通过调用状态更新函数(如上例中的setCount)更新状态时,React会重新渲染组件,并使用最新的状态值。

使用多个状态变量

你可以在一个组件中多次使用useState来声明多个状态变量:

function ExampleWithManyStates() {
  // 声明多个状态变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

  // 此组件可以读取和更新 `age`、`fruit` 和 `todos`
}

注意事项

  • 状态更新可能是异步的:React可能会延迟执行状态更新,以提高性能。因此,如果状态更新依赖于当前状态,最好使用函数形式的更新:

    setCount(currentCount => currentCount + 1);
    
  • 状态更新是替换而不是合并:与类组件中的this.setState方法不同,更新状态变量总是替换它,而不是合并。因此,如果状态是对象或数组,需要注意手动合并或构造新的状态。

通过使用useState,React应用可以在函数组件中以声明式的方式处理局部状态,使得状态管理变得简单直观。

useState原理是什么

useState 是React中用于在函数组件中添加状态的Hook。它的工作原理与React的渲染和调度机制紧密相关。为了理解useState的原理,我们需要深入了解几个关键概念:

1. Hooks 是如何存储状态的

React为每个组件维护了一个状态链表。每次组件渲染时,React都会按照Hooks被调用的顺序,从这个链表中读取或更新状态。这就是为什么Hooks必须在组件的最顶层调用,且不能在循环、条件语句或嵌套函数中调用的原因——这确保了每次组件渲染时Hooks的调用顺序是一致的。

2. 组件渲染和更新

当你使用useState定义了一个状态变量和一个更新这个变量的函数后,每当状态更新函数被调用时,React会将新的状态值入队,然后触发组件的重新渲染。在组件的下一个渲染周期中,useState会返回最新的状态值。

3. 批量更新和异步更新

React对状态更新采用了批处理和异步更新的策略来优化性能。当多个状态更新函数在React事件处理函数或生命周期方法中被同步调用时,React会将这些更新批量处理,延迟更新直到事件处理完成,然后一次性应用所有更新并重新渲染组件。这减少了不必要的渲染次数和计算,提高了应用的性能。

4. 函数式更新

如果新的状态依赖于旧的状态,推荐使用函数式更新。这是因为状态更新可能是异步的,直接使用旧状态可能会导致不一致的结果。函数式更新确保了每次更新都使用最新的状态值。

setCount(prevCount => prevCount + 1);

5. 闭包陷阱

在使用useState(以及其他Hooks)时,需要注意闭包的陷阱。由于函数组件的本质是闭包,所以任何时刻访问的状态值都是某一次渲染时的快照。如果状态更新逻辑依赖于当前的Props或状态,应当使用函数式更新或Effect Hook来确保访问的是最新的Props和状态。

总结

useState通过React的内部状态链表来管理函数组件的状态。通过批处理和异步更新策略,React优化了状态管理的性能。理解这些原理有助于开发者更有效地使用useState和其他Hooks,编写出更高效、更可靠的React应用。

如果初始值是prevCount是0,那么多次执行连续setCount(prevCount => prevCount + 1);,最终值prevCount是多少呢?

如果你在同一个渲染周期中连续多次执行 setCount(prevCount => prevCount + 1);,并且初始值 prevCount 是 0,那么最终的 prevCount 值将取决于你执行该更新函数的次数。

React 会将同一个渲染周期中的所有状态更新批处理(batch)起来。这意味着,如果你连续执行多次状态更新,每一次的更新将基于上一次的结果,即使这些更新是在事件处理器或生命周期方法中同步触发的。因此,如果在同一渲染周期中执行了五次 setCount(prevCount => prevCount + 1);,最终的 prevCount 将会是 5。

这是因为,即使 React 的状态更新可能是异步的,使用函数式更新(即传递一个函数到 setCount)时,React 保证了每一次更新都基于最新的状态值。因此,每次 prevCount => prevCount + 1 调用时,prevCount 都是上一次调用后的最新值。

这与直接设置状态值不同,例如:

for (let i = 0; i < 5; i++) {
  setCount(0 + 1);
}

在这种情况下,无论你执行多少次,最终的状态都会是 1,因为每次更新都是基于相同的初始状态值(即 0)。

总结:在同一渲染周期内连续多次使用函数式更新(例如,setCount(prevCount => prevCount + 1);),并且初始值为 0,执行多少次,最终的 prevCount 就是多少。

React的批处理机制

当在事件处理器、生命周期方法或React的渲染流程中调用setCount(newVal)时,React会将这些更新批量处理。这意味着在这些同步代码块中连续调用setCount(newVal)并不会立即引发渲染。相反,React会累积所有的更新,并在事件处理完毕后一次性应用这些更新,然后进行一次渲染。这是为了性能优化,减少不必要的渲染次数。

异步更新

当在setTimeout、Promise的then回调或原生事件处理函数等异步代码中调用setCount(newVal)时,React 16.x和17中的行为是立即触发更新和渲染的,因为这些场景下React无法自动批处理更新(React 18引入了自动批处理,即使在这些异步场景中也能批处理更新)。

函数组件的重新渲染

setCount(newVal)被调用时,如果newVal与当前状态值相同(React使用Object.is比较算法来检查),React可能会跳过组件的重新渲染。这是因为React认为状态没有变化,因此不需要更新DOM。

多次执行setCount(newVal)

如果在一个同步代码块中多次执行setCount(newVal),并且newVal每次都是相同的值(不依赖于前一个状态的值),由于React的批处理机制,这些更新会被合并,组件只会重新渲染一次,使用最后一次更新的状态值。如果newVal是通过函数式更新计算得到的(例如setCount(prevCount => prevCount + 1)),则每次更新都会基于前一次的状态值,即使更新被批处理,最终的状态也会反映所有的更新。

总结

所以,setCount(newVal)的方式确实可以安排组件的更新和重新渲染,但多次执行setCount(newVal)并不总是导致多次渲染,这取决于React的批处理机制、是否在异步代码中调用以及新旧状态值是否相同。React的目标是优化性能,减少不必要的渲染,同时确保状态的更新和UI的一致性。

个人理解总结:

setCount设置值的时候,引起了函数组件的渲染时,才会将最新的值存入到状态,并为下次函数组件渲染使用,且体现在页面上。所以多次连续的设置setCount,并没有引起多次的函数渲染,导致函数组件闭包中,一直使用的是旧值,尤其是在setCount(count + 1)这种操作。 但是作为一个语言的设计来说,如果想多次设置,在每次设置时都使用最新的count值呢?可以使用setCount(preCount => preCount + 1)。